From dd082660b02511c1068c494da96fa99f3a97d12e Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Fri, 17 Feb 2023 19:27:32 +0800
Subject: [PATCH 01/12] Support node 12

---
 deno.json                                     |  6 +++
 .../node/pagination_js_commonjs/example.js    | 40 +++++++++++--------
 scripts/build_npm.ts                          |  8 +++-
 src/utils.ts                                  | 17 ++++++--
 tests/utils_test.ts                           |  1 +
 5 files changed, 50 insertions(+), 22 deletions(-)

diff --git a/deno.json b/deno.json
index 0aadab2..e5c46d3 100644
--- a/deno.json
+++ b/deno.json
@@ -16,5 +16,11 @@
     "files": {
       "exclude": ["npm/", "examples/node"]
     }
+  },
+  "compilerOptions": {
+    "lib": [
+      "dom",
+      "deno.ns"
+    ]
   }
 }
diff --git a/examples/node/pagination_js_commonjs/example.js b/examples/node/pagination_js_commonjs/example.js
index c0b5065..5b6d02a 100644
--- a/examples/node/pagination_js_commonjs/example.js
+++ b/examples/node/pagination_js_commonjs/example.js
@@ -18,11 +18,13 @@ const run = async () => {
     "First page links",
     extractLinks(page1.organic_results),
   );
-  let page2 = await page1.next?.();
-  console.log(
-    "Second page links",
-    extractLinks(page2?.organic_results),
-  );
+  if (page1.next) {
+    let page2 = await page1.next();
+    console.log(
+      "Second page links",
+      extractLinks(page2.organic_results),
+    );
+  }
 
   // Pagination (callback)
   getJson("google", params, (page1) => {
@@ -30,22 +32,26 @@ const run = async () => {
       "First page links",
       extractLinks(page1.organic_results),
     );
-    page1.next?.((page2) => {
-      console.log(
-        "Second page links",
-        extractLinks(page2.organic_results),
-      );
-    });
+    if (page1.next) {
+      page1.next((page2) => {
+        console.log(
+          "Second page links",
+          extractLinks(page2.organic_results),
+        );
+      });
+    }
   });
 
   // Use global config
   config.api_key = apiKey;
   page1 = await getJson("google", { q: "Coffee" });
-  page2 = await page1.next?.();
-  console.log(
-    "Second page links",
-    extractLinks(page2?.organic_results),
-  );
+  if (page1.next) {
+    page2 = await page1.next();
+    console.log(
+      "Second page links",
+      extractLinks(page2.organic_results),
+    );
+  }
 
   // Pagination loop (async/await)
   let links = [];
@@ -53,7 +59,7 @@ const run = async () => {
   while (page) {
     links.push(...extractLinks(page.organic_results));
     if (links.length >= 30) break;
-    page = await page.next?.();
+    page = page.next ? await page.next(): undefined;
   }
   console.log(links);
 
diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts
index 3bfb545..b145b90 100644
--- a/scripts/build_npm.ts
+++ b/scripts/build_npm.ts
@@ -4,6 +4,7 @@ import { version } from "../version.ts";
 await emptyDir("./npm");
 
 await build({
+  test: false, // Turned off to avoid publishing tests
   entryPoints: ["./mod.ts"],
   rootTestDir: "./tests",
   outDir: "./npm",
@@ -16,8 +17,11 @@ await build({
     // https://deno.land/std/async/delay.ts relies on DOMException.
     // This is only used in tests.
     domException: "dev", //  Only used in tests.
-
-    undici: true, // Required for `fetch`
+  },
+  compilerOptions: {
+    // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping
+    lib: ["es2019"],
+    target: "ES2019",
   },
   package: {
     name: "serpapi",
diff --git a/src/utils.ts b/src/utils.ts
index 1d9bd02..8868a53 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,5 +1,6 @@
 import type { EngineMap } from "./engines/engine_map.ts";
 import { version } from "../version.ts";
+import fetch from "npm:cross-fetch@3.1.4";
 
 type UrlParameters = Record<
   string,
@@ -111,7 +112,7 @@ export function buildUrl<P extends UrlParameters>(
   return `${_internals.getBaseUrl()}${path}?${searchParams}`;
 }
 
-export async function execute<P extends UrlParameters>(
+export function execute<P extends UrlParameters>(
   path: string,
   parameters: P,
   timeout: number,
@@ -120,7 +121,17 @@ export async function execute<P extends UrlParameters>(
     ...parameters,
     source: getSource(),
   });
-  return await _internals.fetch(url, {
-    signal: AbortSignal.timeout(timeout),
+  // https://github.com/github/fetch/issues/175#issuecomment-216791333
+  return new Promise((resolve, reject) => {
+    const timer = setTimeout(() => reject(new Error("Timeout")), timeout);
+    _internals.fetch(url)
+      .then((res) => {
+        clearTimeout(timer);
+        resolve(res);
+      })
+      .catch((err) => {
+        clearTimeout(timer);
+        reject(err);
+      });
   });
 }
diff --git a/tests/utils_test.ts b/tests/utils_test.ts
index 8e27b11..f12c2b3 100644
--- a/tests/utils_test.ts
+++ b/tests/utils_test.ts
@@ -23,6 +23,7 @@ import {
   extractNextParameters,
   haveParametersChanged,
 } from "../src/utils.ts";
+import { Response } from "npm:cross-fetch@3.1.4";
 
 loadSync({ export: true });
 const BASE_URL = Deno.env.get("ENV_TYPE") === "local"

From f2df3db4ddfa04a88ca2352f78667eb870705f4c Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Thu, 16 Feb 2023 16:58:02 +0800
Subject: [PATCH 02/12] Support node 10

---
 scripts/build_npm.ts | 4 ++--
 src/utils.ts         | 7 +++++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts
index b145b90..7820f8f 100644
--- a/scripts/build_npm.ts
+++ b/scripts/build_npm.ts
@@ -20,8 +20,8 @@ await build({
   },
   compilerOptions: {
     // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping
-    lib: ["es2019"],
-    target: "ES2019",
+    lib: ["es2018"],
+    target: "ES2018",
   },
   package: {
     name: "serpapi",
diff --git a/src/utils.ts b/src/utils.ts
index 8868a53..0e528b8 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -54,8 +54,11 @@ export function extractNextParameters<
 
   if (nextUrlString) {
     const nextUrl = new URL(nextUrlString);
-    const nextParameters = Object.fromEntries(nextUrl.searchParams.entries());
-    delete nextParameters["engine"];
+    const nextParameters: Record<string, string> = {};
+    for (const [k, v] of nextUrl.searchParams.entries()) {
+      if (k === "engine") continue;
+      nextParameters[k] = v;
+    }
     return nextParameters as NextParameters<E>;
   }
 }

From 2ca76af96e3f1095c27fd6c471ed570c6eb94720 Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Thu, 16 Feb 2023 20:02:29 +0800
Subject: [PATCH 03/12] Support node 7

---
 examples/node/pagination_js_commonjs/example.js | 10 +++++-----
 scripts/build_npm.ts                            |  5 +++--
 src/utils.ts                                    |  2 ++
 3 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/examples/node/pagination_js_commonjs/example.js b/examples/node/pagination_js_commonjs/example.js
index 5b6d02a..f3b048f 100644
--- a/examples/node/pagination_js_commonjs/example.js
+++ b/examples/node/pagination_js_commonjs/example.js
@@ -16,13 +16,13 @@ const run = async () => {
   let page1 = await getJson("google", params);
   console.log(
     "First page links",
-    extractLinks(page1.organic_results),
+    extractLinks(page1.organic_results)
   );
   if (page1.next) {
     let page2 = await page1.next();
     console.log(
       "Second page links",
-      extractLinks(page2.organic_results),
+      extractLinks(page2.organic_results)
     );
   }
 
@@ -30,13 +30,13 @@ const run = async () => {
   getJson("google", params, (page1) => {
     console.log(
       "First page links",
-      extractLinks(page1.organic_results),
+      extractLinks(page1.organic_results)
     );
     if (page1.next) {
       page1.next((page2) => {
         console.log(
           "Second page links",
-          extractLinks(page2.organic_results),
+          extractLinks(page2.organic_results)
         );
       });
     }
@@ -49,7 +49,7 @@ const run = async () => {
     page2 = await page1.next();
     console.log(
       "Second page links",
-      extractLinks(page2.organic_results),
+      extractLinks(page2.organic_results)
     );
   }
 
diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts
index 7820f8f..2a268da 100644
--- a/scripts/build_npm.ts
+++ b/scripts/build_npm.ts
@@ -5,6 +5,7 @@ await emptyDir("./npm");
 
 await build({
   test: false, // Turned off to avoid publishing tests
+  typeCheck: false,
   entryPoints: ["./mod.ts"],
   rootTestDir: "./tests",
   outDir: "./npm",
@@ -20,8 +21,8 @@ await build({
   },
   compilerOptions: {
     // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping
-    lib: ["es2018"],
-    target: "ES2018",
+    lib: ["es2017"],
+    target: "ES2017",
   },
   package: {
     name: "serpapi",
diff --git a/src/utils.ts b/src/utils.ts
index 0e528b8..16c8b82 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,6 +1,8 @@
 import type { EngineMap } from "./engines/engine_map.ts";
 import { version } from "../version.ts";
 import fetch from "npm:cross-fetch@3.1.4";
+import core from "npm:core-js-pure@3.28.0";
+const { globalThis, URL, URLSearchParams } = core;
 
 type UrlParameters = Record<
   string,

From 5d1a4f9e55cb7b2f6fd6e036f14514ecda69dc9d Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Fri, 17 Feb 2023 18:35:04 +0800
Subject: [PATCH 04/12] Update examples

---
 .../.env.example                                       |  0
 .../{basic_js_esm => basic_js_node_14_up}/README.md    |  4 ++--
 .../{basic_js_esm => basic_js_node_14_up}/example.js   |  8 ++++++++
 .../{basic_js_esm => basic_js_node_14_up}/package.json |  0
 .../{basic_js_esm => basic_js_node_7_up}/.env.example  |  0
 .../README.md                                          |  4 ++--
 .../example.js                                         |  4 ++++
 .../package.json                                       |  0
 .../{basic_ts_esm => basic_ts_node_14_up}/.env.example |  0
 .../README.md                                          |  4 ++--
 .../{basic_ts_esm => basic_ts_node_14_up}/example.ts   |  8 ++++++++
 .../{basic_ts_esm => basic_ts_node_14_up}/package.json |  0
 .../tsconfig.json                                      |  0
 .../.env.example                                       |  0
 .../README.md                                          |  4 ++--
 .../example.js                                         | 10 ++++++++++
 .../package.json                                       |  0
 .../.env.example                                       |  0
 .../README.md                                          |  4 ++--
 .../example.js                                         |  4 ++++
 .../package.json                                       |  0
 .../.env.example                                       |  0
 .../README.md                                          |  4 ++--
 .../example.ts                                         | 10 ++++++++++
 .../package.json                                       |  0
 .../tsconfig.json                                      |  0
 26 files changed, 56 insertions(+), 12 deletions(-)
 rename examples/node/{basic_js_commonjs => basic_js_node_14_up}/.env.example (100%)
 rename examples/node/{basic_js_esm => basic_js_node_14_up}/README.md (88%)
 rename examples/node/{basic_js_esm => basic_js_node_14_up}/example.js (59%)
 rename examples/node/{basic_js_esm => basic_js_node_14_up}/package.json (100%)
 rename examples/node/{basic_js_esm => basic_js_node_7_up}/.env.example (100%)
 rename examples/node/{basic_js_commonjs => basic_js_node_7_up}/README.md (88%)
 rename examples/node/{basic_js_commonjs => basic_js_node_7_up}/example.js (92%)
 rename examples/node/{basic_js_commonjs => basic_js_node_7_up}/package.json (100%)
 rename examples/node/{basic_ts_esm => basic_ts_node_14_up}/.env.example (100%)
 rename examples/node/{pagination_ts_esm => basic_ts_node_14_up}/README.md (89%)
 rename examples/node/{basic_ts_esm => basic_ts_node_14_up}/example.ts (60%)
 rename examples/node/{basic_ts_esm => basic_ts_node_14_up}/package.json (100%)
 rename examples/node/{basic_ts_esm => basic_ts_node_14_up}/tsconfig.json (100%)
 rename examples/node/{pagination_js_commonjs => pagination_js_node_14_up}/.env.example (100%)
 rename examples/node/{pagination_js_esm => pagination_js_node_14_up}/README.md (87%)
 rename examples/node/{pagination_js_esm => pagination_js_node_14_up}/example.js (71%)
 rename examples/node/{pagination_js_esm => pagination_js_node_14_up}/package.json (100%)
 rename examples/node/{pagination_js_esm => pagination_js_node_7_up}/.env.example (100%)
 rename examples/node/{pagination_js_commonjs => pagination_js_node_7_up}/README.md (86%)
 rename examples/node/{pagination_js_commonjs => pagination_js_node_7_up}/example.js (97%)
 rename examples/node/{pagination_js_commonjs => pagination_js_node_7_up}/package.json (100%)
 rename examples/node/{pagination_ts_esm => pagination_ts_node_14_up}/.env.example (100%)
 rename examples/node/{basic_ts_esm => pagination_ts_node_14_up}/README.md (88%)
 rename examples/node/{pagination_ts_esm => pagination_ts_node_14_up}/example.ts (72%)
 rename examples/node/{pagination_ts_esm => pagination_ts_node_14_up}/package.json (100%)
 rename examples/node/{pagination_ts_esm => pagination_ts_node_14_up}/tsconfig.json (100%)

diff --git a/examples/node/basic_js_commonjs/.env.example b/examples/node/basic_js_node_14_up/.env.example
similarity index 100%
rename from examples/node/basic_js_commonjs/.env.example
rename to examples/node/basic_js_node_14_up/.env.example
diff --git a/examples/node/basic_js_esm/README.md b/examples/node/basic_js_node_14_up/README.md
similarity index 88%
rename from examples/node/basic_js_esm/README.md
rename to examples/node/basic_js_node_14_up/README.md
index aed73cb..2818395 100644
--- a/examples/node/basic_js_esm/README.md
+++ b/examples/node/basic_js_node_14_up/README.md
@@ -1,4 +1,4 @@
-# Node.js basic JavaScript (ESM) example
+# Basic JavaScript example for Node.js 14 and newer
 
 ## Usage
 
@@ -17,7 +17,7 @@ deno task npm
 3. Install dependencies and run example
 
 ```bash
-cd examples/node/basic_js_esm
+cd examples/node/basic_js_node_14_up
 npm i
 npm start
 ```
diff --git a/examples/node/basic_js_esm/example.js b/examples/node/basic_js_node_14_up/example.js
similarity index 59%
rename from examples/node/basic_js_esm/example.js
rename to examples/node/basic_js_node_14_up/example.js
index caab132..bb171f7 100644
--- a/examples/node/basic_js_esm/example.js
+++ b/examples/node/basic_js_node_14_up/example.js
@@ -1,3 +1,11 @@
+/**
+ * Example works for Node.js 14 and newer.
+ * - Uses ESM imports which is supported from Node.js 13.2.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility
+ * - Uses top-level await which is supported from Node.js 14.8.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility
+ */
+
 import * as Dotenv from "dotenv";
 import { config, getJson } from "serpapi";
 
diff --git a/examples/node/basic_js_esm/package.json b/examples/node/basic_js_node_14_up/package.json
similarity index 100%
rename from examples/node/basic_js_esm/package.json
rename to examples/node/basic_js_node_14_up/package.json
diff --git a/examples/node/basic_js_esm/.env.example b/examples/node/basic_js_node_7_up/.env.example
similarity index 100%
rename from examples/node/basic_js_esm/.env.example
rename to examples/node/basic_js_node_7_up/.env.example
diff --git a/examples/node/basic_js_commonjs/README.md b/examples/node/basic_js_node_7_up/README.md
similarity index 88%
rename from examples/node/basic_js_commonjs/README.md
rename to examples/node/basic_js_node_7_up/README.md
index 2ded14b..3b73b0c 100644
--- a/examples/node/basic_js_commonjs/README.md
+++ b/examples/node/basic_js_node_7_up/README.md
@@ -1,4 +1,4 @@
-# Node.js basic JavaScript (CommonJS) example
+# Basic JavaScript example for Node.js 7 and newer
 
 ## Usage
 
@@ -17,7 +17,7 @@ deno task npm
 3. Install dependencies and run example
 
 ```bash
-cd examples/node/basic_js_commonjs
+cd examples/node/basic_js_node_7_up
 npm i
 npm start
 ```
diff --git a/examples/node/basic_js_commonjs/example.js b/examples/node/basic_js_node_7_up/example.js
similarity index 92%
rename from examples/node/basic_js_commonjs/example.js
rename to examples/node/basic_js_node_7_up/example.js
index 43136a8..301baf4 100644
--- a/examples/node/basic_js_commonjs/example.js
+++ b/examples/node/basic_js_node_7_up/example.js
@@ -1,3 +1,7 @@
+/**
+ * Example works for Node.js 7 and newer.
+ */
+
 const Dotenv = require("dotenv");
 const { config, getJson } = require("serpapi");
 
diff --git a/examples/node/basic_js_commonjs/package.json b/examples/node/basic_js_node_7_up/package.json
similarity index 100%
rename from examples/node/basic_js_commonjs/package.json
rename to examples/node/basic_js_node_7_up/package.json
diff --git a/examples/node/basic_ts_esm/.env.example b/examples/node/basic_ts_node_14_up/.env.example
similarity index 100%
rename from examples/node/basic_ts_esm/.env.example
rename to examples/node/basic_ts_node_14_up/.env.example
diff --git a/examples/node/pagination_ts_esm/README.md b/examples/node/basic_ts_node_14_up/README.md
similarity index 89%
rename from examples/node/pagination_ts_esm/README.md
rename to examples/node/basic_ts_node_14_up/README.md
index 96e2230..44fd48a 100644
--- a/examples/node/pagination_ts_esm/README.md
+++ b/examples/node/basic_ts_node_14_up/README.md
@@ -1,4 +1,4 @@
-# Node.js pagination TypeScript (ESM) example
+# Basic TypeScript example for Node.js 14 and newer
 
 ## Usage
 
@@ -17,7 +17,7 @@ deno task npm
 3. Install dependencies and run example
 
 ```bash
-cd examples/node/pagination_ts_esm
+cd examples/node/basic_ts_node_14_up
 npm i
 npm start
 ```
diff --git a/examples/node/basic_ts_esm/example.ts b/examples/node/basic_ts_node_14_up/example.ts
similarity index 60%
rename from examples/node/basic_ts_esm/example.ts
rename to examples/node/basic_ts_node_14_up/example.ts
index 7faa58b..09b35b6 100644
--- a/examples/node/basic_ts_esm/example.ts
+++ b/examples/node/basic_ts_node_14_up/example.ts
@@ -1,3 +1,11 @@
+/**
+ * Example works for Node.js 14 and newer.
+ * - Uses ESM imports which is supported from Node.js 13.2.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility
+ * - Uses top-level await which is supported from Node.js 14.8.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility
+ */
+
 import * as Dotenv from "dotenv";
 import { config, getJson, GoogleParameters } from "serpapi";
 
diff --git a/examples/node/basic_ts_esm/package.json b/examples/node/basic_ts_node_14_up/package.json
similarity index 100%
rename from examples/node/basic_ts_esm/package.json
rename to examples/node/basic_ts_node_14_up/package.json
diff --git a/examples/node/basic_ts_esm/tsconfig.json b/examples/node/basic_ts_node_14_up/tsconfig.json
similarity index 100%
rename from examples/node/basic_ts_esm/tsconfig.json
rename to examples/node/basic_ts_node_14_up/tsconfig.json
diff --git a/examples/node/pagination_js_commonjs/.env.example b/examples/node/pagination_js_node_14_up/.env.example
similarity index 100%
rename from examples/node/pagination_js_commonjs/.env.example
rename to examples/node/pagination_js_node_14_up/.env.example
diff --git a/examples/node/pagination_js_esm/README.md b/examples/node/pagination_js_node_14_up/README.md
similarity index 87%
rename from examples/node/pagination_js_esm/README.md
rename to examples/node/pagination_js_node_14_up/README.md
index 4e4ebe2..81a9eb1 100644
--- a/examples/node/pagination_js_esm/README.md
+++ b/examples/node/pagination_js_node_14_up/README.md
@@ -1,4 +1,4 @@
-# Node.js pagination JavaScript (ESM) example
+# Pagination JavaScript example for Node.js 14 and newer
 
 ## Usage
 
@@ -17,7 +17,7 @@ deno task npm
 3. Install dependencies and run example
 
 ```bash
-cd examples/node/pagination_js_esm
+cd examples/node/pagination_js_node_14_up
 npm i
 npm start
 ```
diff --git a/examples/node/pagination_js_esm/example.js b/examples/node/pagination_js_node_14_up/example.js
similarity index 71%
rename from examples/node/pagination_js_esm/example.js
rename to examples/node/pagination_js_node_14_up/example.js
index d5bf869..650a3e5 100644
--- a/examples/node/pagination_js_esm/example.js
+++ b/examples/node/pagination_js_node_14_up/example.js
@@ -1,3 +1,13 @@
+/**
+ * Example works for Node.js 14 and newer.
+ * - Uses ESM imports which is supported from Node.js 13.2.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility
+ * - Uses top-level await which is supported from Node.js 14.8.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility
+ * - Uses optional chaining which is supported from Node.js 14.0.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#browser_compatibility
+ */
+
 import * as Dotenv from "dotenv";
 import { config, getJson } from "serpapi";
 
diff --git a/examples/node/pagination_js_esm/package.json b/examples/node/pagination_js_node_14_up/package.json
similarity index 100%
rename from examples/node/pagination_js_esm/package.json
rename to examples/node/pagination_js_node_14_up/package.json
diff --git a/examples/node/pagination_js_esm/.env.example b/examples/node/pagination_js_node_7_up/.env.example
similarity index 100%
rename from examples/node/pagination_js_esm/.env.example
rename to examples/node/pagination_js_node_7_up/.env.example
diff --git a/examples/node/pagination_js_commonjs/README.md b/examples/node/pagination_js_node_7_up/README.md
similarity index 86%
rename from examples/node/pagination_js_commonjs/README.md
rename to examples/node/pagination_js_node_7_up/README.md
index 0afbe7a..c5e9810 100644
--- a/examples/node/pagination_js_commonjs/README.md
+++ b/examples/node/pagination_js_node_7_up/README.md
@@ -1,4 +1,4 @@
-# Node.js pagination JavaScript (CommonJS) example
+# Pagination JavaScript example for Node.js 7 and newer
 
 ## Usage
 
@@ -17,7 +17,7 @@ deno task npm
 3. Install dependencies and run example
 
 ```bash
-cd examples/node/pagination_js_commonjs
+cd examples/node/pagination_js_node_7_up
 npm i
 npm start
 ```
diff --git a/examples/node/pagination_js_commonjs/example.js b/examples/node/pagination_js_node_7_up/example.js
similarity index 97%
rename from examples/node/pagination_js_commonjs/example.js
rename to examples/node/pagination_js_node_7_up/example.js
index f3b048f..f998b37 100644
--- a/examples/node/pagination_js_commonjs/example.js
+++ b/examples/node/pagination_js_node_7_up/example.js
@@ -1,3 +1,7 @@
+/**
+ * Example works for Node.js 7 and newer.
+ */
+
 const Dotenv = require("dotenv");
 const { config, getJson } = require("serpapi");
 
diff --git a/examples/node/pagination_js_commonjs/package.json b/examples/node/pagination_js_node_7_up/package.json
similarity index 100%
rename from examples/node/pagination_js_commonjs/package.json
rename to examples/node/pagination_js_node_7_up/package.json
diff --git a/examples/node/pagination_ts_esm/.env.example b/examples/node/pagination_ts_node_14_up/.env.example
similarity index 100%
rename from examples/node/pagination_ts_esm/.env.example
rename to examples/node/pagination_ts_node_14_up/.env.example
diff --git a/examples/node/basic_ts_esm/README.md b/examples/node/pagination_ts_node_14_up/README.md
similarity index 88%
rename from examples/node/basic_ts_esm/README.md
rename to examples/node/pagination_ts_node_14_up/README.md
index 3c2ec7e..c95d849 100644
--- a/examples/node/basic_ts_esm/README.md
+++ b/examples/node/pagination_ts_node_14_up/README.md
@@ -1,4 +1,4 @@
-# Node.js basic TypeScript (ESM) example
+# Pagination TypeScript example for Node.js 14 and newer
 
 ## Usage
 
@@ -17,7 +17,7 @@ deno task npm
 3. Install dependencies and run example
 
 ```bash
-cd examples/node/basic_ts_esm
+cd examples/node/pagination_ts_node_14_up
 npm i
 npm start
 ```
diff --git a/examples/node/pagination_ts_esm/example.ts b/examples/node/pagination_ts_node_14_up/example.ts
similarity index 72%
rename from examples/node/pagination_ts_esm/example.ts
rename to examples/node/pagination_ts_node_14_up/example.ts
index cb5858d..7d3ec91 100644
--- a/examples/node/pagination_ts_esm/example.ts
+++ b/examples/node/pagination_ts_node_14_up/example.ts
@@ -1,3 +1,13 @@
+/**
+ * Example works for Node.js 14 and newer.
+ * - Uses ESM imports which is supported from Node.js 13.2.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility
+ * - Uses top-level await which is supported from Node.js 14.8.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility
+ * - Uses optional chaining which is supported from Node.js 14.0.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#browser_compatibility
+ */
+
 import * as Dotenv from "dotenv";
 import { config, getJson, GoogleParameters } from "serpapi";
 
diff --git a/examples/node/pagination_ts_esm/package.json b/examples/node/pagination_ts_node_14_up/package.json
similarity index 100%
rename from examples/node/pagination_ts_esm/package.json
rename to examples/node/pagination_ts_node_14_up/package.json
diff --git a/examples/node/pagination_ts_esm/tsconfig.json b/examples/node/pagination_ts_node_14_up/tsconfig.json
similarity index 100%
rename from examples/node/pagination_ts_esm/tsconfig.json
rename to examples/node/pagination_ts_node_14_up/tsconfig.json

From 36f6ba6e46dad436073d843ba4f908ff87e6fa4d Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Fri, 24 Feb 2023 19:42:18 +0800
Subject: [PATCH 05/12] Update docs

---
 CHANGELOG.md                                  |  2 ++
 README.md                                     | 33 +++++++++++++++++--
 ...ating_from_google_search_results_nodejs.md |  2 --
 3 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 098518c..d4a2eca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to
 
 ### Added
 
+- Add support for Node.js 7.10.1 and newer.
+
 ### Changed
 
 - Remove `settings.json`, update CONTRIBUTING.md.
diff --git a/README.md b/README.md
index 469f571..9056db9 100644
--- a/README.md
+++ b/README.md
@@ -17,12 +17,39 @@ more.
 
 ### Node.js
 
-Ensure you're running at least Node.js v16.14.
+- Supports Node.js 7.10.1 and newer.
+- Refer to [this example](examples/node/basic_js_node_7_up) for help.
 
 ```bash
 npm install serpapi
 ```
 
+```js
+const { getJson } = require("serpapi");
+getJson("google", {
+  api_key: API_KEY, // Get your API_KEY from https://serpapi.com/manage-api-key
+  q: "coffee",
+  location: "Austin, Texas",
+}, (json) => {
+  console.log(json["organic_results"]);
+});
+```
+
+### Node.js with ES Modules (ESM) and top-level await
+
+- If you prefer using the `import` syntax and top-level `await`, you need to use
+  at least Node.js 14.8.0.
+- Refer to [this example](examples/node/basic_js_node_14_up) for help.
+
+You will need to add `"type": "module"` to your `package.json`:
+
+```js
+{
+  "type": "module",
+  // rest of package.json
+}
+```
+
 ```js
 import { getJson } from "serpapi";
 const response = await getJson("google", {
@@ -35,7 +62,9 @@ console.log(response);
 
 ### Deno
 
-Import directly from deno.land. Usage is otherwise the same as above.
+- Import directly from deno.land.
+- Usage is otherwise the same as above.
+- Refer to [this example](examples/deno/basic_ts) for help.
 
 ```ts
 import { getJson } from "https://deno.land/x/serpapi/mod.ts";
diff --git a/docs/migrating_from_google_search_results_nodejs.md b/docs/migrating_from_google_search_results_nodejs.md
index 2e869ab..6ef7f5d 100644
--- a/docs/migrating_from_google_search_results_nodejs.md
+++ b/docs/migrating_from_google_search_results_nodejs.md
@@ -51,8 +51,6 @@ migrate over to the `serpapi` npm package.
 - The `buildUrl`, `execute` and `search` methods are removed. Use `getJson` and
   `getHtml` functions instead.
 - The `SerpApiSearch` class is removed as a public class.
-- Dropped support for Node.js 16.13 and below. This module supports Node.js
-  16.14 and above.
 
 ## Fixed
 

From 95be440ae0df55f2cc681e82b99db929e732bb82 Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Mon, 27 Feb 2023 12:45:42 +0800
Subject: [PATCH 06/12] Update examples to depend on published npm module

---
 examples/node/basic_js_node_14_up/README.md   | 29 ++--------------
 .../node/basic_js_node_14_up/package.json     |  2 +-
 examples/node/basic_js_node_7_up/README.md    | 28 ++--------------
 examples/node/basic_js_node_7_up/package.json |  2 +-
 examples/node/basic_ts_node_14_up/README.md   | 33 ++-----------------
 .../node/basic_ts_node_14_up/package.json     |  2 +-
 .../node/pagination_js_node_14_up/README.md   | 29 ++--------------
 .../pagination_js_node_14_up/package.json     |  2 +-
 .../node/pagination_js_node_7_up/README.md    | 28 ++--------------
 .../node/pagination_js_node_7_up/package.json |  2 +-
 .../node/pagination_ts_node_14_up/README.md   | 33 ++-----------------
 .../pagination_ts_node_14_up/package.json     |  2 +-
 12 files changed, 18 insertions(+), 174 deletions(-)

diff --git a/examples/node/basic_js_node_14_up/README.md b/examples/node/basic_js_node_14_up/README.md
index 2818395..3b36e40 100644
--- a/examples/node/basic_js_node_14_up/README.md
+++ b/examples/node/basic_js_node_14_up/README.md
@@ -2,39 +2,14 @@
 
 ## Usage
 
-1. Build module
-
-```bash
-cd ../../../   # Navigate to the root folder
-deno task npm
-```
-
-2. Setup environment variables
+1. Setup environment variables
 
 - Duplicate `.env.example` and name it `.env`.
 - Replace `YOUR_API_KEY` with your SerpApi API key.
 
-3. Install dependencies and run example
+2. Install dependencies and run example
 
 ```bash
-cd examples/node/basic_js_node_14_up
 npm i
 npm start
 ```
-
-## Notes
-
-- If you want to run the example without building the module, you can update
-  `package.json` to depend on the published `serpapi` npm module instead:
-  ```json
-  {
-    "type": "module",
-    "dependencies": {
-      "dotenv": "*",
-      "serpapi": "*" // Relies on the npm module
-    },
-    "scripts": {
-      "start": "node example.js"
-    }
-  }
-  ```
diff --git a/examples/node/basic_js_node_14_up/package.json b/examples/node/basic_js_node_14_up/package.json
index ddd9788..055dcb9 100644
--- a/examples/node/basic_js_node_14_up/package.json
+++ b/examples/node/basic_js_node_14_up/package.json
@@ -2,7 +2,7 @@
   "type": "module",
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "*"
   },
   "scripts": {
     "start": "node example.js"
diff --git a/examples/node/basic_js_node_7_up/README.md b/examples/node/basic_js_node_7_up/README.md
index 3b73b0c..9085457 100644
--- a/examples/node/basic_js_node_7_up/README.md
+++ b/examples/node/basic_js_node_7_up/README.md
@@ -2,38 +2,14 @@
 
 ## Usage
 
-1. Build module
-
-```bash
-cd ../../../   # Navigate to the root folder
-deno task npm
-```
-
-2. Setup environment variables
+1. Setup environment variables
 
 - Duplicate `.env.example` and name it `.env`.
 - Replace `YOUR_API_KEY` with your SerpApi API key.
 
-3. Install dependencies and run example
+2. Install dependencies and run example
 
 ```bash
-cd examples/node/basic_js_node_7_up
 npm i
 npm start
 ```
-
-## Notes
-
-- If you want to run the example without building the module, you can update
-  `package.json` to depend on the published `serpapi` npm module instead:
-  ```json
-  {
-    "dependencies": {
-      "dotenv": "*",
-      "serpapi": "*" // Relies on the npm module
-    },
-    "scripts": {
-      "start": "node example.js"
-    }
-  }
-  ```
diff --git a/examples/node/basic_js_node_7_up/package.json b/examples/node/basic_js_node_7_up/package.json
index 6a5d1e4..f7ceb03 100644
--- a/examples/node/basic_js_node_7_up/package.json
+++ b/examples/node/basic_js_node_7_up/package.json
@@ -1,7 +1,7 @@
 {
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "*"
   },
   "scripts": {
     "start": "node example.js"
diff --git a/examples/node/basic_ts_node_14_up/README.md b/examples/node/basic_ts_node_14_up/README.md
index 44fd48a..20e12ba 100644
--- a/examples/node/basic_ts_node_14_up/README.md
+++ b/examples/node/basic_ts_node_14_up/README.md
@@ -2,43 +2,14 @@
 
 ## Usage
 
-1. Build module
-
-```bash
-cd ../../../   # Navigate to the root folder
-deno task npm
-```
-
-2. Setup environment variables
+1. Setup environment variables
 
 - Duplicate `.env.example` and name it `.env`.
 - Replace `YOUR_API_KEY` with your SerpApi API key.
 
-3. Install dependencies and run example
+2. Install dependencies and run example
 
 ```bash
-cd examples/node/basic_ts_node_14_up
 npm i
 npm start
 ```
-
-## Notes
-
-- If you want to run the example without building the module, you can update
-  `package.json` to depend on the published `serpapi` npm module instead:
-  ```json
-  {
-    "type": "module",
-    "dependencies": {
-      "dotenv": "*",
-      "serpapi": "*" // Relies on the npm module
-    },
-    "devDependencies": {
-      "@types/node": "*",
-      "typescript": "*"
-    },
-    "scripts": {
-      "start": "npx ts-node example.ts"
-    }
-  }
-  ```
diff --git a/examples/node/basic_ts_node_14_up/package.json b/examples/node/basic_ts_node_14_up/package.json
index dee2799..e1ef349 100644
--- a/examples/node/basic_ts_node_14_up/package.json
+++ b/examples/node/basic_ts_node_14_up/package.json
@@ -2,7 +2,7 @@
   "type": "module",
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "*"
   },
   "devDependencies": {
     "@types/node": "*",
diff --git a/examples/node/pagination_js_node_14_up/README.md b/examples/node/pagination_js_node_14_up/README.md
index 81a9eb1..d49b924 100644
--- a/examples/node/pagination_js_node_14_up/README.md
+++ b/examples/node/pagination_js_node_14_up/README.md
@@ -2,39 +2,14 @@
 
 ## Usage
 
-1. Build module
-
-```bash
-cd ../../../   # Navigate to the root folder
-deno task npm
-```
-
-2. Setup environment variables
+1. Setup environment variables
 
 - Duplicate `.env.example` and name it `.env`.
 - Replace `YOUR_API_KEY` with your SerpApi API key.
 
-3. Install dependencies and run example
+2. Install dependencies and run example
 
 ```bash
-cd examples/node/pagination_js_node_14_up
 npm i
 npm start
 ```
-
-## Notes
-
-- If you want to run the example without building the module, you can update
-  `package.json` to depend on the published `serpapi` npm module instead:
-  ```json
-  {
-    "type": "module",
-    "dependencies": {
-      "dotenv": "*",
-      "serpapi": "*" // Relies on the npm module
-    },
-    "scripts": {
-      "start": "node example.js"
-    }
-  }
-  ```
diff --git a/examples/node/pagination_js_node_14_up/package.json b/examples/node/pagination_js_node_14_up/package.json
index ddd9788..055dcb9 100644
--- a/examples/node/pagination_js_node_14_up/package.json
+++ b/examples/node/pagination_js_node_14_up/package.json
@@ -2,7 +2,7 @@
   "type": "module",
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "*"
   },
   "scripts": {
     "start": "node example.js"
diff --git a/examples/node/pagination_js_node_7_up/README.md b/examples/node/pagination_js_node_7_up/README.md
index c5e9810..4542a22 100644
--- a/examples/node/pagination_js_node_7_up/README.md
+++ b/examples/node/pagination_js_node_7_up/README.md
@@ -2,38 +2,14 @@
 
 ## Usage
 
-1. Build module
-
-```bash
-cd ../../../   # Navigate to the root folder
-deno task npm
-```
-
-2. Setup environment variables
+1. Setup environment variables
 
 - Duplicate `.env.example` and name it `.env`.
 - Replace `YOUR_API_KEY` with your SerpApi API key.
 
-3. Install dependencies and run example
+2. Install dependencies and run example
 
 ```bash
-cd examples/node/pagination_js_node_7_up
 npm i
 npm start
 ```
-
-## Notes
-
-- If you want to run the example without building the module, you can update
-  `package.json` to depend on the published `serpapi` npm module instead:
-  ```json
-  {
-    "dependencies": {
-      "dotenv": "*",
-      "serpapi": "*" // Relies on the npm module
-    },
-    "scripts": {
-      "start": "node example.js"
-    }
-  }
-  ```
diff --git a/examples/node/pagination_js_node_7_up/package.json b/examples/node/pagination_js_node_7_up/package.json
index 6a5d1e4..f7ceb03 100644
--- a/examples/node/pagination_js_node_7_up/package.json
+++ b/examples/node/pagination_js_node_7_up/package.json
@@ -1,7 +1,7 @@
 {
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "*"
   },
   "scripts": {
     "start": "node example.js"
diff --git a/examples/node/pagination_ts_node_14_up/README.md b/examples/node/pagination_ts_node_14_up/README.md
index c95d849..2f6cb33 100644
--- a/examples/node/pagination_ts_node_14_up/README.md
+++ b/examples/node/pagination_ts_node_14_up/README.md
@@ -2,43 +2,14 @@
 
 ## Usage
 
-1. Build module
-
-```bash
-cd ../../../   # Navigate to the root folder
-deno task npm
-```
-
-2. Setup environment variables
+1. Setup environment variables
 
 - Duplicate `.env.example` and name it `.env`.
 - Replace `YOUR_API_KEY` with your SerpApi API key.
 
-3. Install dependencies and run example
+2. Install dependencies and run example
 
 ```bash
-cd examples/node/pagination_ts_node_14_up
 npm i
 npm start
 ```
-
-## Notes
-
-- If you want to run the example without building the module, you can update
-  `package.json` to depend on the published `serpapi` npm module instead:
-  ```json
-  {
-    "type": "module",
-    "dependencies": {
-      "dotenv": "*",
-      "serpapi": "*" // Relies on the npm module
-    },
-    "devDependencies": {
-      "@types/node": "*",
-      "typescript": "*"
-    },
-    "scripts": {
-      "start": "npx ts-node example.ts"
-    }
-  }
-  ```
diff --git a/examples/node/pagination_ts_node_14_up/package.json b/examples/node/pagination_ts_node_14_up/package.json
index dee2799..e1ef349 100644
--- a/examples/node/pagination_ts_node_14_up/package.json
+++ b/examples/node/pagination_ts_node_14_up/package.json
@@ -2,7 +2,7 @@
   "type": "module",
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "*"
   },
   "devDependencies": {
     "@types/node": "*",

From 5afe18e37b2588484c7988c12f36930bf6da28d0 Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Mon, 27 Feb 2023 12:46:58 +0800
Subject: [PATCH 07/12] Add docs on running examples on local files

---
 CONTRIBUTING.md | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 29bbfa4..bb18f40 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -96,6 +96,25 @@ deno task test:cov     # Get test coverage by running tests that hit "localhost"
 deno task test:ci      # Run tests that hit "https://serpapi.com"
 ```
 
+## Run examples on local source files
+
+To run [examples](./examples/) on your local source files, follow these steps.
+
+1. Run `deno task npm` to build the files.
+2. Update the respective example's `package.json` to depend on the local
+   `serpapi` module instead,
+   ```json
+   {
+     "dependencies": {
+       "dotenv": "*",
+       "serpapi": "../../../npm"
+     },
+     "scripts": {
+       "start": "node example.js"
+     }
+   }
+   ```
+
 ## Update documentation
 
 - Every exposed function must have associated JSDoc comments.
@@ -112,7 +131,8 @@ deno task docs:gen
 TypeScript types are generated from the backend code. Follow these steps to
 update the types.
 
-1. Run `bundle exec rails sdk:generate_ts_types` in the backend repository.
+1. Run `bundle exec rails libraries:generate_ts_types` in the backend
+   repository.
 2. Replace everything in `src/engines` with the generated files from
    `tmp/ts/engines`.
 3. Update `mod.ts` with the new engine exports from `tmp/ts/mod.ts`.

From 96311e7c2b07188004dc3e20777e7903bd8435e4 Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Thu, 2 Mar 2023 13:15:06 +0800
Subject: [PATCH 08/12] Add automated smoke tests

---
 .github/workflows/build.yml       |  81 ++++++++++++++++-
 CONTRIBUTING.md                   |  17 +++-
 deno.json                         |  10 ++-
 tests/smoke/.gitignore            |   3 +
 tests/smoke/commonjs/.env.example |   1 +
 tests/smoke/commonjs/commonjs.js  | 139 ++++++++++++++++++++++++++++++
 tests/smoke/commonjs/package.json |   9 ++
 tests/smoke/esm/.env.example      |   1 +
 tests/smoke/esm/esm.js            | 130 ++++++++++++++++++++++++++++
 tests/smoke/esm/package.json      |  10 +++
 10 files changed, 396 insertions(+), 5 deletions(-)
 create mode 100644 tests/smoke/.gitignore
 create mode 100644 tests/smoke/commonjs/.env.example
 create mode 100644 tests/smoke/commonjs/commonjs.js
 create mode 100644 tests/smoke/commonjs/package.json
 create mode 100644 tests/smoke/esm/.env.example
 create mode 100644 tests/smoke/esm/esm.js
 create mode 100644 tests/smoke/esm/package.json

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 880d6f8..8e8ce83 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,6 +4,7 @@ on: push
 
 jobs:
   build:
+    name: "Deno tests and build npm files"
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout repo
@@ -28,8 +29,84 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v3
         with:
-          node-version: '18.x'
+          node-version: '18.x'  # Build files using a fixed node version
           registry-url: 'https://registry.npmjs.org'
 
-      - name: Test building of npm files
+      - name: Build npm files
         run: deno task npm
+
+      - name: Zip build files
+        run: zip npm.zip ./npm -r
+
+      - name: Upload build files for smoke tests
+        uses: actions/upload-artifact@v3
+        with:
+          name: npm
+          path: npm.zip
+          retention-days: 1
+
+  smoke-tests-commonjs:
+    name: "Smoke tests (CommonJS)"
+    needs: build
+    runs-on: ubuntu-22.04
+    strategy:
+      matrix:
+        node-version: [7.x, 8.x, 9.x, 10.x, 11.x, 12.x, 13.x, 14.x, 15.x, 16.x, 17.x, 18.x, 19.x]
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v3
+
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Download build files
+        uses: actions/download-artifact@v3
+        with:
+          name: npm
+
+      - name: Unzip build files
+        run: unzip npm.zip
+
+      - name: Run smoke tests
+        env:
+          SERPAPI_TEST_KEY: ${{ secrets.SERPAPI_TEST_KEY }}
+        run: |
+          cd tests/smoke/commonjs
+          npm i
+          npm test
+
+  smoke-tests-esm:
+    name: "Smoke tests (ESM)"
+    needs: build
+    runs-on: ubuntu-22.04
+    strategy:
+      matrix:
+        node-version: [14.x, 15.x, 16.x, 17.x, 18.x, 19.x]
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v3
+
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Download build files
+        uses: actions/download-artifact@v3
+        with:
+          name: npm
+
+      - name: Unzip build files
+        run: unzip npm.zip
+
+      - name: Run smoke tests
+        env:
+          SERPAPI_TEST_KEY: ${{ secrets.SERPAPI_TEST_KEY }}
+        run: |
+          cd tests/smoke/esm
+          npm i
+          npm test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bb18f40..9fb98dd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,7 +26,8 @@ If you use VSCode, use the following settings (`.vscode/settings.json`):
     "mod.ts",
     "version.ts",
     "src",
-    "tests",
+    "tests/*.ts",
+    "tests/engines/",
     "scripts",
     "examples/deno"
   ],
@@ -115,6 +116,20 @@ To run [examples](./examples/) on your local source files, follow these steps.
    }
    ```
 
+## Run smoke tests
+
+These test key functionality on different Node.js versions. They are ran on
+GitHub Actions, see the [build workflow](.github/workflows/build.yml) for more
+details.
+
+To run these locally, follow these steps.
+
+1. Run `deno task npm` to build the files.
+2. Change directory to either the `commonjs` or `esm` folder.
+3. Setup the intended Node.js version. For example, if you're using `nvm`, you
+   can run `nvm use 14` to run Node.js 14 for the current shell.
+4. Run `npm i`, then `npm test`.
+
 ## Update documentation
 
 - Every exposed function must have associated JSDoc comments.
diff --git a/deno.json b/deno.json
index e5c46d3..6e8fb32 100644
--- a/deno.json
+++ b/deno.json
@@ -9,12 +9,18 @@
   },
   "fmt": {
     "files": {
-      "exclude": ["npm/", "examples/node"]
+      "exclude": ["npm/", "examples/node", "tests/smoke/"]
     }
   },
   "lint": {
     "files": {
-      "exclude": ["npm/", "examples/node"]
+      "exclude": ["npm/", "examples/node", "tests/smoke/"]
+    }
+  },
+  "test": {
+    "files": {
+      "include": ["tests/"],
+      "exclude": ["tests/smoke/"]
     }
   },
   "compilerOptions": {
diff --git a/tests/smoke/.gitignore b/tests/smoke/.gitignore
new file mode 100644
index 0000000..217965e
--- /dev/null
+++ b/tests/smoke/.gitignore
@@ -0,0 +1,3 @@
+# Smoke tests run on different node versions which generate different
+# package-lock.json files.
+package-lock.json
\ No newline at end of file
diff --git a/tests/smoke/commonjs/.env.example b/tests/smoke/commonjs/.env.example
new file mode 100644
index 0000000..6debcc7
--- /dev/null
+++ b/tests/smoke/commonjs/.env.example
@@ -0,0 +1 @@
+SERPAPI_TEST_KEY=YOUR_API_KEY
\ No newline at end of file
diff --git a/tests/smoke/commonjs/commonjs.js b/tests/smoke/commonjs/commonjs.js
new file mode 100644
index 0000000..9cf60b1
--- /dev/null
+++ b/tests/smoke/commonjs/commonjs.js
@@ -0,0 +1,139 @@
+/**
+ * Smoke test works for Node.js 7 and newer.
+ */
+
+const Dotenv = require("dotenv");
+const { config, getJson, getHtml, getJsonBySearchId, getHtmlBySearchId, getAccount, getLocations } = require("serpapi");
+
+Dotenv.config();
+const apiKey = process.env.SERPAPI_TEST_KEY;
+
+const run = async () => {
+  console.log("running", process.versions.node);
+  
+  const params = {
+    q: "Coffee",
+    api_key: apiKey,
+  };
+
+  let searchId;
+  
+  {
+    console.log("getJson async await");
+    const page1 = await getJson("google", params);
+    searchId = page1["search_metadata"]["id"];
+    if (!page1["organic_results"]) throw new Error("No organic results");
+    if (page1.next) {
+      const page2 = await page1.next();
+      if (!page2["organic_results"]) throw new Error("No organic results");
+    }
+  }
+
+  {
+    console.log("getJson callback");
+    getJson("google", params, (page1) => {
+      if (!page1["organic_results"]) throw new Error("No organic results");
+      if (page1.next) {
+        page1.next((page2) => {
+          if (!page2["organic_results"]) throw new Error("No organic results");
+        });
+      }
+    });
+  }
+
+  {
+    console.log("getJson using global config");
+    config.api_key = apiKey;
+    const page1 = await getJson("google", { q: "Coffee" });
+    if (!page1["organic_results"]) throw new Error("No organic results");
+    if (page1.next) {
+      const page2 = await page1.next();
+      if (!page2["organic_results"]) throw new Error("No organic results");
+    }
+  }
+
+  {
+    console.log("getJson pagination loop (async/await)");
+    const links = [];
+    let page = await getJson("google", params);
+    while (page) {
+      links.push(...page.organic_results.map(r => r.link));
+      if (links.length >= 30) break;
+      page = page.next ? await page.next(): undefined;
+    }
+    if (links.length < 30) throw new Error("Incorrect number of links");
+  }
+  
+  {
+    console.log("getJson pagination loop (callback)");
+    const links = [];
+    getJson("google", params, (page) => {
+      links.push(...page.organic_results.map(r => r.link));
+      if (links.length < 30 && page.next) {
+        page.next();
+      } else {
+        if (links.length < 30) throw new Error("Incorrect number of links");
+      }
+    });
+  }
+
+  {
+    console.log("getHtml");
+    const html = await getHtml("google", params);
+    if (html.length < 1000) throw new Error("Incorrect HTML");
+    
+    getHtml("google", params, html => {
+      if (html.length < 1000) throw new Error("Incorrect HTML");
+    });
+  }
+
+  {
+    console.log("getJsonBySearchId");
+    config.api_key = apiKey;
+    const json = await getJsonBySearchId(searchId);
+    if (!json["organic_results"]) throw new Error("No organic results");
+
+    getJsonBySearchId(searchId, {}, (json) => {
+      if (!json["organic_results"]) throw new Error("No organic results");
+    });
+  }
+
+  {
+    console.log("getHtmlBySearchId");
+    config.api_key = apiKey;
+    const html = await getHtmlBySearchId(searchId);
+    if (html.length < 1000) throw new Error("Incorrect HTML");
+
+    getHtmlBySearchId(searchId, {}, (html) => {
+      if (html.length < 1000) throw new Error("Incorrect HTML");
+    });
+  }
+
+  {
+    console.log("getAccount");
+    config.api_key = apiKey;
+    const info = await getAccount();
+    if (!info["account_email"]) throw new Error("Incorrect account info");
+
+    getAccount({}, (info) => {
+      if (!info["account_email"]) throw new Error("Incorrect account info");
+    });
+  }
+
+  {
+    console.log("getLocations");
+    const locations = await getLocations({ limit: 3 });
+    if (locations.length !== 3) throw new Error("Incorrect locations length");
+    
+    getLocations({ limit: 3 }, (locations) => {
+      if (locations.length !== 3) throw new Error("Incorrect locations length");
+    });
+  }
+
+  console.log("success", process.versions.node);
+};
+
+run().catch((e) => {
+  console.error(e);
+  process.exitCode = 1;
+});
\ No newline at end of file
diff --git a/tests/smoke/commonjs/package.json b/tests/smoke/commonjs/package.json
new file mode 100644
index 0000000..1d5cecb
--- /dev/null
+++ b/tests/smoke/commonjs/package.json
@@ -0,0 +1,9 @@
+{
+  "dependencies": {
+    "dotenv": "*",
+    "serpapi": "../../../npm"
+  },
+  "scripts": {
+    "test": "node commonjs.js"
+  }
+}
diff --git a/tests/smoke/esm/.env.example b/tests/smoke/esm/.env.example
new file mode 100644
index 0000000..6debcc7
--- /dev/null
+++ b/tests/smoke/esm/.env.example
@@ -0,0 +1 @@
+SERPAPI_TEST_KEY=YOUR_API_KEY
\ No newline at end of file
diff --git a/tests/smoke/esm/esm.js b/tests/smoke/esm/esm.js
new file mode 100644
index 0000000..914cfc2
--- /dev/null
+++ b/tests/smoke/esm/esm.js
@@ -0,0 +1,130 @@
+/**
+ * Smoke test works for Node.js 14 and newer.
+ * - Uses ESM imports which is supported from Node.js 13.2.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility
+ * - Uses top-level await which is supported from Node.js 14.8.0.
+ *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility
+ */
+
+import Dotenv from "dotenv";
+import { config, getJson, getHtml, getJsonBySearchId, getHtmlBySearchId, getAccount, getLocations } from "serpapi";
+
+Dotenv.config();
+const apiKey = process.env.SERPAPI_TEST_KEY;
+
+console.log("running", process.versions.node);
+
+const params = {
+  q: "Coffee",
+  api_key: apiKey,
+};
+
+let searchId;
+
+{
+  console.log("getJson async await")
+  const page1 = await getJson("google", params);
+  searchId = page1["search_metadata"]["id"];
+  if (!page1["organic_results"]) throw new Error("No organic results");
+  const page2 = await page1.next?.();
+  if (!page2["organic_results"]) throw new Error("No organic results");
+}
+
+{
+  console.log("getJson callback")
+  getJson("google", params, (page1) => {
+    if (!page1["organic_results"]) throw new Error("No organic results");
+    page1.next?.((page2) => {
+      if (!page2["organic_results"]) throw new Error("No organic results");
+    });
+  });
+}
+
+{
+  console.log("getJson using global config")
+  config.api_key = apiKey;
+  const page1 = await getJson("google", { q: "Coffee" });
+  if (!page1["organic_results"]) throw new Error("No organic results");
+  const page2 = await page1.next?.();
+  if (!page2["organic_results"]) throw new Error("No organic results");
+}
+
+{
+  console.log("getJson pagination loop (async/await)")
+  const links = [];
+  let page = await getJson("google", params);
+  while (page) {
+    links.push(...page.organic_results.map(r => r.link));
+    if (links.length >= 30) break;
+    page = await page.next?.();
+  }
+  if (links.length < 30) throw new Error("Incorrect number of links");
+}
+
+{
+  console.log("getJson pagination loop (callback)")
+  const links = [];
+  getJson("google", params, (page) => {
+    links.push(...page.organic_results.map(r => r.link));
+    if (links.length < 30 && page.next) {
+      page.next();
+    } else {
+      if (links.length < 30) throw new Error("Incorrect number of links");
+    }
+  });
+}
+
+{
+  console.log("getHtml")
+  const html = await getHtml("google", params);
+  if (html.length < 1000) throw new Error("Incorrect HTML");
+  
+  getHtml("google", params, html => {
+    if (html.length < 1000) throw new Error("Incorrect HTML");
+  });
+}
+
+{
+  console.log("getJsonBySearchId")
+  config.api_key = apiKey;
+  const json = await getJsonBySearchId(searchId);
+  if (!json["organic_results"]) throw new Error("No organic results");
+
+  getJsonBySearchId(searchId, {}, (json) => {
+    if (!json["organic_results"]) throw new Error("No organic results");
+  });
+}
+
+{
+  console.log("getHtmlBySearchId")
+  config.api_key = apiKey;
+  const html = await getHtmlBySearchId(searchId);
+  if (html.length < 1000) throw new Error("Incorrect HTML");
+
+  getHtmlBySearchId(searchId, {}, (html) => {
+    if (html.length < 1000) throw new Error("Incorrect HTML");
+  });
+}
+
+{
+  console.log("getAccount")
+  config.api_key = apiKey;
+  const info = await getAccount();
+  if (!info["account_email"]) throw new Error("Incorrect account info");
+
+  getAccount({}, (info) => {
+    if (!info["account_email"]) throw new Error("Incorrect account info");
+  });
+}
+
+{
+  console.log("getLocations")
+  const locations = await getLocations({ limit: 3 });
+  if (locations.length !== 3) throw new Error("Incorrect locations length");
+  
+  getLocations({ limit: 3 }, (locations) => {
+    if (locations.length !== 3) throw new Error("Incorrect locations length");
+  });
+}
+
+console.log("success", process.versions.node);
diff --git a/tests/smoke/esm/package.json b/tests/smoke/esm/package.json
new file mode 100644
index 0000000..f80c76b
--- /dev/null
+++ b/tests/smoke/esm/package.json
@@ -0,0 +1,10 @@
+{
+  "type": "module",
+  "dependencies": {
+    "dotenv": "*",
+    "serpapi": "../../../npm"
+  },
+  "scripts": {
+    "test": "node esm.js"
+  }
+}

From 8155edcd26c2bdaefa11535316dbe803657f73d3 Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Tue, 7 Mar 2023 13:55:49 +0800
Subject: [PATCH 09/12] Temporarily disable home depot tests

---
 tests/engines/home_depot_test.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/engines/home_depot_test.ts b/tests/engines/home_depot_test.ts
index 15665d3..918e0a1 100644
--- a/tests/engines/home_depot_test.ts
+++ b/tests/engines/home_depot_test.ts
@@ -33,6 +33,7 @@ const BASE_URL = Deno.env.get("ENV_TYPE") === "local"
 describe("home_depot", {
   sanitizeOps: false, // TODO(seb): look into how we can avoid setting these to false
   sanitizeResources: false,
+  ignore: true,
 }, () => {
   let urlStub: Stub;
   const engine = "home_depot";

From 56b285cc6e0622115b03ab9c28df9570590953ee Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Tue, 7 Mar 2023 14:56:54 +0800
Subject: [PATCH 10/12] Use polyfills only if they don't exist in the runtime

---
 deno.json           |  1 +
 src/serpapi.ts      |  2 +-
 src/utils.ts        | 99 +++++++++++++++++++++++++++------------------
 tests/utils_test.ts | 71 ++++++++++++++------------------
 4 files changed, 93 insertions(+), 80 deletions(-)

diff --git a/deno.json b/deno.json
index 6e8fb32..cd7448c 100644
--- a/deno.json
+++ b/deno.json
@@ -26,6 +26,7 @@
   "compilerOptions": {
     "lib": [
       "dom",
+      "dom.iterable",
       "deno.ns"
     ]
   }
diff --git a/src/serpapi.ts b/src/serpapi.ts
index 124c129..b22ddfa 100644
--- a/src/serpapi.ts
+++ b/src/serpapi.ts
@@ -90,7 +90,7 @@ export async function getJson<
   const json = await response.json() as BaseResponse<
     EngineMap[E]["parameters"]
   >;
-  const nextParametersFromResponse = extractNextParameters<E>(json);
+  const nextParametersFromResponse = await extractNextParameters<E>(json);
   if (
     // https://github.com/serpapi/public-roadmap/issues/562
     // https://github.com/serpapi/public-roadmap/issues/563
diff --git a/src/utils.ts b/src/utils.ts
index 16c8b82..535f970 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,8 +1,5 @@
 import type { EngineMap } from "./engines/engine_map.ts";
 import { version } from "../version.ts";
-import fetch from "npm:cross-fetch@3.1.4";
-import core from "npm:core-js-pure@3.28.0";
-const { globalThis, URL, URLSearchParams } = core;
 
 type UrlParameters = Record<
   string,
@@ -11,26 +8,48 @@ type UrlParameters = Record<
 
 /**
  * This `_internals` object is needed to support stubbing/spying of
- * fetch, execute and getBaseUrl.
+ * certain functions in this file.
  * https://deno.land/manual@v1.28.3/basics/testing/mocking
  *
- * If fetch is stubbed via `globalThis`, the test phase of the npm build fails.
- * ```ts
- * const fetchStub = stub(globalThis, "fetch", resolvesNext([new Response("data")]));
- * ```
- *
- * [`dnt`](https://github.com/denoland/dnt) shims `fetch` by relying on the
- * `undici` package. It replaces all references to `fetch` with `dntShim.fetch`.
- * As a side effect, stubbing `globalThis.fetch` becomes incorrect; we want to
- * stub `dntShim.fetch` instead.
- *
- * As a workaround, the `_internals` object serves as an indirection and we
- * stub the `fetch` key of this object instead.
+ * It's also useful to encapsulate functions that are polyfilled.
  */
 export const _internals = {
-  fetch: fetch,
+  get fetch() {
+    return (async () => {
+      // Use runtime's `fetch` if it exists, otherwise fallback to `cross-fetch`.
+      return typeof fetch === "function"
+        ? Promise.resolve(fetch)
+        : (await import("npm:cross-fetch@3.1.4")).default;
+    })();
+  },
   execute: execute,
   getBaseUrl: getBaseUrl,
+  get globalThis() {
+    return (async () => {
+      // Use runtime's `globalThis` if it exists, otherwise fallback to `core-js`'s implementation.
+      // dnt-shim-ignore
+      const gt = typeof globalThis !== "undefined" ? globalThis : undefined;
+      return gt !== undefined
+        ? Promise.resolve(gt)
+        : (await import("npm:core-js-pure@3.28.0")).default.globalThis;
+    })();
+  },
+  get URL(): Promise<typeof URL> {
+    return (async () => {
+      // Use runtime's `URL` if it exists, otherwise fallback to `core-js`'s implementation.
+      return typeof URL !== "undefined"
+        ? Promise.resolve(URL)
+        : (await import("npm:core-js-pure@3.28.0")).default.URL;
+    })();
+  },
+  get URLSearchParams(): Promise<typeof URLSearchParams> {
+    return (async () => {
+      // Use runtime's `URLSearchParams` if it exists, otherwise fallback to `core-js`'s implementation.
+      return typeof URLSearchParams !== "undefined"
+        ? Promise.resolve(URLSearchParams)
+        : (await import("npm:core-js-pure@3.28.0")).default.URLSearchParams;
+    })();
+  },
 };
 
 /** Facilitates stubbing in tests, e.g. localhost as the base url */
@@ -45,7 +64,7 @@ type NextParametersKeys<E extends keyof EngineMap> = Omit<
 type NextParameters<E extends keyof EngineMap> = {
   [K in keyof NextParametersKeys<E>]: string;
 };
-export function extractNextParameters<
+export async function extractNextParameters<
   E extends keyof EngineMap,
 >(json: {
   serpapi_pagination?: { next: string };
@@ -55,6 +74,7 @@ export function extractNextParameters<
     json["pagination"]?.["next"];
 
   if (nextUrlString) {
+    const URL = await _internals.URL;
     const nextUrl = new URL(nextUrlString);
     const nextParameters: Record<string, string> = {};
     for (const [k, v] of nextUrl.searchParams.entries()) {
@@ -79,22 +99,20 @@ export function haveParametersChanged(
   );
 }
 
-function getSource() {
+export async function getSource() {
   const moduleSource = `serpapi@${version}`;
   try {
+    const gt = await _internals.globalThis;
+
     // Check if running in Node.js
-    // dnt-shim-ignore
-    // deno-lint-ignore no-explicit-any
-    const nodeVersion = (globalThis as any).process?.versions?.node;
+    const nodeVersion = gt.process?.versions?.node;
     if (nodeVersion) {
       return `nodejs@${nodeVersion},${moduleSource}`;
     }
 
     // Assumes running in Deno instead. https://deno.land/api?s=Deno.version
     // Deno.version is not shimmed since it's not used when ran in a Node env.
-    // dnt-shim-ignore
-    // deno-lint-ignore no-explicit-any
-    const denoVersion = (globalThis as any).Deno?.version?.deno;
+    const denoVersion = gt.Deno?.version?.deno;
     if (denoVersion) {
       return `deno@${denoVersion},${moduleSource}`;
     }
@@ -106,37 +124,40 @@ function getSource() {
   }
 }
 
-export function buildUrl<P extends UrlParameters>(
+export async function buildUrl<P extends UrlParameters>(
   path: string,
   parameters: P,
-): string {
+): Promise<string> {
   const nonUndefinedParams: [string, string][] = Object.entries(parameters)
     .filter(([_, value]) => value !== undefined)
     .map(([key, value]) => [key, `${value}`]);
+  const URLSearchParams = await _internals.URLSearchParams;
   const searchParams = new URLSearchParams(nonUndefinedParams);
   return `${_internals.getBaseUrl()}${path}?${searchParams}`;
 }
 
-export function execute<P extends UrlParameters>(
+export async function execute<P extends UrlParameters>(
   path: string,
   parameters: P,
   timeout: number,
 ): Promise<Response> {
-  const url = buildUrl(path, {
+  const url = await buildUrl(path, {
     ...parameters,
-    source: getSource(),
+    source: await getSource(),
   });
   // https://github.com/github/fetch/issues/175#issuecomment-216791333
   return new Promise((resolve, reject) => {
     const timer = setTimeout(() => reject(new Error("Timeout")), timeout);
-    _internals.fetch(url)
-      .then((res) => {
-        clearTimeout(timer);
-        resolve(res);
-      })
-      .catch((err) => {
-        clearTimeout(timer);
-        reject(err);
-      });
+    _internals.fetch.then((fetch) =>
+      fetch(url)
+        .then((res) => {
+          clearTimeout(timer);
+          resolve(res);
+        })
+        .catch((err) => {
+          clearTimeout(timer);
+          reject(err);
+        })
+    );
   });
 }
diff --git a/tests/utils_test.ts b/tests/utils_test.ts
index f12c2b3..ff689d2 100644
--- a/tests/utils_test.ts
+++ b/tests/utils_test.ts
@@ -5,12 +5,7 @@ import {
   describe,
   it,
 } from "https://deno.land/std@0.170.0/testing/bdd.ts";
-import {
-  assertSpyCalls,
-  resolvesNext,
-  Stub,
-  stub,
-} from "https://deno.land/std@0.170.0/testing/mock.ts";
+import { Stub, stub } from "https://deno.land/std@0.170.0/testing/mock.ts";
 import {
   assertEquals,
   assertMatch,
@@ -21,9 +16,9 @@ import {
   buildUrl,
   execute,
   extractNextParameters,
+  getSource,
   haveParametersChanged,
 } from "../src/utils.ts";
-import { Response } from "npm:cross-fetch@3.1.4";
 
 loadSync({ export: true });
 const BASE_URL = Deno.env.get("ENV_TYPE") === "local"
@@ -31,9 +26,9 @@ const BASE_URL = Deno.env.get("ENV_TYPE") === "local"
   : "https://serpapi.com";
 
 describe("extractNextParameters", () => {
-  it("with serpapi_pagination property", () => {
+  it("with serpapi_pagination property", async () => {
     assertEquals(
-      extractNextParameters<"google">({
+      await extractNextParameters<"google">({
         serpapi_pagination: {
           next:
             "https://serpapi.com/search.json?device=desktop&engine=google&gl=us&google_domain=google.com&hl=en&location=Austin%2C+Texas%2C+United+States&q=coffee&start=10",
@@ -51,9 +46,9 @@ describe("extractNextParameters", () => {
     );
   });
 
-  it("with pagination property", () => {
+  it("with pagination property", async () => {
     assertEquals(
-      extractNextParameters<"google_scholar_profiles">(
+      await extractNextParameters<"google_scholar_profiles">(
         {
           pagination: {
             next:
@@ -170,6 +165,15 @@ describe("haveParametersChanged", () => {
   });
 });
 
+describe("getSource", () => {
+  it("use runtime version", async () => {
+    assertMatch(
+      await getSource(),
+      /(nodejs|deno)@\d+\.\d+\.\d+,serpapi@\d+\.\d+\.\d+$/,
+    );
+  });
+});
+
 describe("buildUrl", () => {
   let urlStub: Stub;
 
@@ -181,24 +185,32 @@ describe("buildUrl", () => {
     urlStub.restore();
   });
 
-  it("with blank path and empty parameters", () => {
-    assertEquals(buildUrl("", {}), `${BASE_URL}?`);
+  it("with blank path and empty parameters", async () => {
+    assertEquals(await buildUrl("", {}), `${BASE_URL}?`);
   });
 
-  it("with path and empty parameters", () => {
-    assertEquals(buildUrl("/", {}), `${BASE_URL}/?`);
+  it("with path and empty parameters", async () => {
+    assertEquals(await buildUrl("/", {}), `${BASE_URL}/?`);
   });
 
-  it("with path and parameters", () => {
+  it("with path and parameters", async () => {
     assertEquals(
-      buildUrl("/search", { q: "coffee", gl: "us" }),
+      await buildUrl("/search", { q: "coffee", gl: "us" }),
       `${BASE_URL}/search?q=coffee&gl=us`,
     );
   });
 
-  it("with undefined parameters", () => {
+  it("with source", async () => {
+    const url = await buildUrl("/search", { source: await getSource() });
+    assertMatch(
+      url,
+      /source=(nodejs|deno)%40\d+\.\d+\.\d+%2Cserpapi%40\d+\.\d+\.\d+$/,
+    );
+  });
+
+  it("with undefined parameters", async () => {
     assertEquals(
-      buildUrl("/search", { q: "coffee", gl: undefined, hl: null }),
+      await buildUrl("/search", { q: "coffee", gl: undefined, hl: null }),
       `${BASE_URL}/search?q=coffee&hl=null`,
     );
   });
@@ -218,27 +230,6 @@ describe("execute", {
     urlStub.restore();
   });
 
-  it("with path and parameters calls fetch with source appended", async () => {
-    const fetchStub = stub(
-      _internals,
-      "fetch",
-      resolvesNext([new Response("data")]),
-    );
-    try {
-      await execute("/search", { q: "coffee", gl: "us" }, 4000);
-    } finally {
-      fetchStub.restore();
-    }
-
-    assertSpyCalls(fetchStub, 1);
-    const url = fetchStub.calls[0].args[0] as string;
-    // e.g. deno@1.28.2,serpapi@1.0.0
-    assertMatch(
-      url,
-      /source=(nodejs|deno)%40\d+\.\d+\.\d+%2Cserpapi%40\d+\.\d+\.\d+$/,
-    );
-  });
-
   it("with short timeout", () => {
     assertRejects(async () =>
       await execute("/search", { q: "coffee", gl: "us" }, 1)

From 58890967de05fd7cd01c8b7220f2dedb4db839fb Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Tue, 7 Mar 2023 17:10:16 +0800
Subject: [PATCH 11/12] Move smoke tests + format

---
 .github/workflows/build.yml                   |  4 +-
 deno.json                                     |  7 ++--
 {tests/smoke => smoke_tests}/.gitignore       |  0
 .../commonjs/.env.example                     |  0
 .../commonjs/commonjs.js                      | 30 +++++++++-----
 .../commonjs/package.json                     |  2 +-
 {tests/smoke => smoke_tests}/esm/.env.example |  0
 {tests/smoke => smoke_tests}/esm/esm.js       | 40 +++++++++++--------
 {tests/smoke => smoke_tests}/esm/package.json |  2 +-
 9 files changed, 50 insertions(+), 35 deletions(-)
 rename {tests/smoke => smoke_tests}/.gitignore (100%)
 rename {tests/smoke => smoke_tests}/commonjs/.env.example (100%)
 rename {tests/smoke => smoke_tests}/commonjs/commonjs.js (90%)
 rename {tests/smoke => smoke_tests}/commonjs/package.json (76%)
 rename {tests/smoke => smoke_tests}/esm/.env.example (100%)
 rename {tests/smoke => smoke_tests}/esm/esm.js (82%)
 rename {tests/smoke => smoke_tests}/esm/package.json (79%)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8e8ce83..4ca5687 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -74,7 +74,7 @@ jobs:
         env:
           SERPAPI_TEST_KEY: ${{ secrets.SERPAPI_TEST_KEY }}
         run: |
-          cd tests/smoke/commonjs
+          cd smoke_tests/commonjs
           npm i
           npm test
 
@@ -107,6 +107,6 @@ jobs:
         env:
           SERPAPI_TEST_KEY: ${{ secrets.SERPAPI_TEST_KEY }}
         run: |
-          cd tests/smoke/esm
+          cd smoke_tests/esm
           npm i
           npm test
diff --git a/deno.json b/deno.json
index cd7448c..93afa4a 100644
--- a/deno.json
+++ b/deno.json
@@ -9,18 +9,17 @@
   },
   "fmt": {
     "files": {
-      "exclude": ["npm/", "examples/node", "tests/smoke/"]
+      "exclude": ["npm/", "examples/node", "smoke_tests/"]
     }
   },
   "lint": {
     "files": {
-      "exclude": ["npm/", "examples/node", "tests/smoke/"]
+      "exclude": ["npm/", "examples/node", "smoke_tests/"]
     }
   },
   "test": {
     "files": {
-      "include": ["tests/"],
-      "exclude": ["tests/smoke/"]
+      "include": ["tests/"]
     }
   },
   "compilerOptions": {
diff --git a/tests/smoke/.gitignore b/smoke_tests/.gitignore
similarity index 100%
rename from tests/smoke/.gitignore
rename to smoke_tests/.gitignore
diff --git a/tests/smoke/commonjs/.env.example b/smoke_tests/commonjs/.env.example
similarity index 100%
rename from tests/smoke/commonjs/.env.example
rename to smoke_tests/commonjs/.env.example
diff --git a/tests/smoke/commonjs/commonjs.js b/smoke_tests/commonjs/commonjs.js
similarity index 90%
rename from tests/smoke/commonjs/commonjs.js
rename to smoke_tests/commonjs/commonjs.js
index 9cf60b1..9c5fef0 100644
--- a/tests/smoke/commonjs/commonjs.js
+++ b/smoke_tests/commonjs/commonjs.js
@@ -3,21 +3,29 @@
  */
 
 const Dotenv = require("dotenv");
-const { config, getJson, getHtml, getJsonBySearchId, getHtmlBySearchId, getAccount, getLocations } = require("serpapi");
+const {
+  config,
+  getJson,
+  getHtml,
+  getJsonBySearchId,
+  getHtmlBySearchId,
+  getAccount,
+  getLocations,
+} = require("serpapi");
 
 Dotenv.config();
 const apiKey = process.env.SERPAPI_TEST_KEY;
 
 const run = async () => {
   console.log("running", process.versions.node);
-  
+
   const params = {
     q: "Coffee",
     api_key: apiKey,
   };
 
   let searchId;
-  
+
   {
     console.log("getJson async await");
     const page1 = await getJson("google", params);
@@ -57,18 +65,18 @@ const run = async () => {
     const links = [];
     let page = await getJson("google", params);
     while (page) {
-      links.push(...page.organic_results.map(r => r.link));
+      links.push(...page.organic_results.map((r) => r.link));
       if (links.length >= 30) break;
-      page = page.next ? await page.next(): undefined;
+      page = page.next ? await page.next() : undefined;
     }
     if (links.length < 30) throw new Error("Incorrect number of links");
   }
-  
+
   {
     console.log("getJson pagination loop (callback)");
     const links = [];
     getJson("google", params, (page) => {
-      links.push(...page.organic_results.map(r => r.link));
+      links.push(...page.organic_results.map((r) => r.link));
       if (links.length < 30 && page.next) {
         page.next();
       } else {
@@ -81,8 +89,8 @@ const run = async () => {
     console.log("getHtml");
     const html = await getHtml("google", params);
     if (html.length < 1000) throw new Error("Incorrect HTML");
-    
-    getHtml("google", params, html => {
+
+    getHtml("google", params, (html) => {
       if (html.length < 1000) throw new Error("Incorrect HTML");
     });
   }
@@ -124,7 +132,7 @@ const run = async () => {
     console.log("getLocations");
     const locations = await getLocations({ limit: 3 });
     if (locations.length !== 3) throw new Error("Incorrect locations length");
-    
+
     getLocations({ limit: 3 }, (locations) => {
       if (locations.length !== 3) throw new Error("Incorrect locations length");
     });
@@ -136,4 +144,4 @@ const run = async () => {
 run().catch((e) => {
   console.error(e);
   process.exitCode = 1;
-});
\ No newline at end of file
+});
diff --git a/tests/smoke/commonjs/package.json b/smoke_tests/commonjs/package.json
similarity index 76%
rename from tests/smoke/commonjs/package.json
rename to smoke_tests/commonjs/package.json
index 1d5cecb..f229cf1 100644
--- a/tests/smoke/commonjs/package.json
+++ b/smoke_tests/commonjs/package.json
@@ -1,7 +1,7 @@
 {
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "../../npm"
   },
   "scripts": {
     "test": "node commonjs.js"
diff --git a/tests/smoke/esm/.env.example b/smoke_tests/esm/.env.example
similarity index 100%
rename from tests/smoke/esm/.env.example
rename to smoke_tests/esm/.env.example
diff --git a/tests/smoke/esm/esm.js b/smoke_tests/esm/esm.js
similarity index 82%
rename from tests/smoke/esm/esm.js
rename to smoke_tests/esm/esm.js
index 914cfc2..1dd8c69 100644
--- a/tests/smoke/esm/esm.js
+++ b/smoke_tests/esm/esm.js
@@ -7,7 +7,15 @@
  */
 
 import Dotenv from "dotenv";
-import { config, getJson, getHtml, getJsonBySearchId, getHtmlBySearchId, getAccount, getLocations } from "serpapi";
+import {
+  config,
+  getAccount,
+  getHtml,
+  getHtmlBySearchId,
+  getJson,
+  getJsonBySearchId,
+  getLocations,
+} from "serpapi";
 
 Dotenv.config();
 const apiKey = process.env.SERPAPI_TEST_KEY;
@@ -22,7 +30,7 @@ const params = {
 let searchId;
 
 {
-  console.log("getJson async await")
+  console.log("getJson async await");
   const page1 = await getJson("google", params);
   searchId = page1["search_metadata"]["id"];
   if (!page1["organic_results"]) throw new Error("No organic results");
@@ -31,7 +39,7 @@ let searchId;
 }
 
 {
-  console.log("getJson callback")
+  console.log("getJson callback");
   getJson("google", params, (page1) => {
     if (!page1["organic_results"]) throw new Error("No organic results");
     page1.next?.((page2) => {
@@ -41,7 +49,7 @@ let searchId;
 }
 
 {
-  console.log("getJson using global config")
+  console.log("getJson using global config");
   config.api_key = apiKey;
   const page1 = await getJson("google", { q: "Coffee" });
   if (!page1["organic_results"]) throw new Error("No organic results");
@@ -50,11 +58,11 @@ let searchId;
 }
 
 {
-  console.log("getJson pagination loop (async/await)")
+  console.log("getJson pagination loop (async/await)");
   const links = [];
   let page = await getJson("google", params);
   while (page) {
-    links.push(...page.organic_results.map(r => r.link));
+    links.push(...page.organic_results.map((r) => r.link));
     if (links.length >= 30) break;
     page = await page.next?.();
   }
@@ -62,10 +70,10 @@ let searchId;
 }
 
 {
-  console.log("getJson pagination loop (callback)")
+  console.log("getJson pagination loop (callback)");
   const links = [];
   getJson("google", params, (page) => {
-    links.push(...page.organic_results.map(r => r.link));
+    links.push(...page.organic_results.map((r) => r.link));
     if (links.length < 30 && page.next) {
       page.next();
     } else {
@@ -75,17 +83,17 @@ let searchId;
 }
 
 {
-  console.log("getHtml")
+  console.log("getHtml");
   const html = await getHtml("google", params);
   if (html.length < 1000) throw new Error("Incorrect HTML");
-  
-  getHtml("google", params, html => {
+
+  getHtml("google", params, (html) => {
     if (html.length < 1000) throw new Error("Incorrect HTML");
   });
 }
 
 {
-  console.log("getJsonBySearchId")
+  console.log("getJsonBySearchId");
   config.api_key = apiKey;
   const json = await getJsonBySearchId(searchId);
   if (!json["organic_results"]) throw new Error("No organic results");
@@ -96,7 +104,7 @@ let searchId;
 }
 
 {
-  console.log("getHtmlBySearchId")
+  console.log("getHtmlBySearchId");
   config.api_key = apiKey;
   const html = await getHtmlBySearchId(searchId);
   if (html.length < 1000) throw new Error("Incorrect HTML");
@@ -107,7 +115,7 @@ let searchId;
 }
 
 {
-  console.log("getAccount")
+  console.log("getAccount");
   config.api_key = apiKey;
   const info = await getAccount();
   if (!info["account_email"]) throw new Error("Incorrect account info");
@@ -118,10 +126,10 @@ let searchId;
 }
 
 {
-  console.log("getLocations")
+  console.log("getLocations");
   const locations = await getLocations({ limit: 3 });
   if (locations.length !== 3) throw new Error("Incorrect locations length");
-  
+
   getLocations({ limit: 3 }, (locations) => {
     if (locations.length !== 3) throw new Error("Incorrect locations length");
   });
diff --git a/tests/smoke/esm/package.json b/smoke_tests/esm/package.json
similarity index 79%
rename from tests/smoke/esm/package.json
rename to smoke_tests/esm/package.json
index f80c76b..aaae393 100644
--- a/tests/smoke/esm/package.json
+++ b/smoke_tests/esm/package.json
@@ -2,7 +2,7 @@
   "type": "module",
   "dependencies": {
     "dotenv": "*",
-    "serpapi": "../../../npm"
+    "serpapi": "../../npm"
   },
   "scripts": {
     "test": "node esm.js"

From 37a468ebfe90b7e1ae29098c7236d875a2bd183c Mon Sep 17 00:00:00 2001
From: Sebastian Quek <sebq13@gmail.com>
Date: Tue, 7 Mar 2023 15:28:41 +0800
Subject: [PATCH 12/12] Support Vercel Edge fns and CloudFlare Workers

---
 deno.json       |  3 +++
 src/global.d.ts |  7 +++++++
 src/utils.ts    | 20 +++++++++++++++++---
 3 files changed, 27 insertions(+), 3 deletions(-)
 create mode 100644 src/global.d.ts

diff --git a/deno.json b/deno.json
index 93afa4a..613d2eb 100644
--- a/deno.json
+++ b/deno.json
@@ -23,6 +23,9 @@
     }
   },
   "compilerOptions": {
+    "types": [
+      "./src/global.d.ts"
+    ],
     "lib": [
       "dom",
       "dom.iterable",
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..ec2b416
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,7 @@
+/**
+ * Used to check if the module is being ran on Vercel Edge functions:
+ * https://vercel.com/docs/concepts/functions/edge-functions/edge-runtime#check-if-you're-running-on-the-edge-runtime
+ *
+ * This declaration exists so that Deno is aware of this possible global variable.
+ */
+declare const EdgeRuntime: string | undefined;
diff --git a/src/utils.ts b/src/utils.ts
index 535f970..5e76143 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -17,6 +17,7 @@ export const _internals = {
   get fetch() {
     return (async () => {
       // Use runtime's `fetch` if it exists, otherwise fallback to `cross-fetch`.
+      // Ensures that it works for Vercel Edge functions and CloudFlare Workers.
       return typeof fetch === "function"
         ? Promise.resolve(fetch)
         : (await import("npm:cross-fetch@3.1.4")).default;
@@ -27,9 +28,13 @@ export const _internals = {
   get globalThis() {
     return (async () => {
       // Use runtime's `globalThis` if it exists, otherwise fallback to `core-js`'s implementation.
+      // Note that `typeof EdgeRuntime === "string"` cannot be extracted to a
+      // shared constant as Next.js will somehow resolve that shared constant
+      // to be falsy. This causes the build to detect a disallowed
+      // "Dynamic Code Evaluation" which fails.
       // dnt-shim-ignore
       const gt = typeof globalThis !== "undefined" ? globalThis : undefined;
-      return gt !== undefined
+      return ((gt !== undefined) || typeof EdgeRuntime === "string")
         ? Promise.resolve(gt)
         : (await import("npm:core-js-pure@3.28.0")).default.globalThis;
     })();
@@ -37,7 +42,7 @@ export const _internals = {
   get URL(): Promise<typeof URL> {
     return (async () => {
       // Use runtime's `URL` if it exists, otherwise fallback to `core-js`'s implementation.
-      return typeof URL !== "undefined"
+      return ((typeof EdgeRuntime === "string") || typeof URL !== "undefined")
         ? Promise.resolve(URL)
         : (await import("npm:core-js-pure@3.28.0")).default.URL;
     })();
@@ -45,7 +50,8 @@ export const _internals = {
   get URLSearchParams(): Promise<typeof URLSearchParams> {
     return (async () => {
       // Use runtime's `URLSearchParams` if it exists, otherwise fallback to `core-js`'s implementation.
-      return typeof URLSearchParams !== "undefined"
+      return ((typeof EdgeRuntime === "string") ||
+          typeof URLSearchParams !== "undefined")
         ? Promise.resolve(URLSearchParams)
         : (await import("npm:core-js-pure@3.28.0")).default.URLSearchParams;
     })();
@@ -117,6 +123,14 @@ export async function getSource() {
       return `deno@${denoVersion},${moduleSource}`;
     }
 
+    if (typeof EdgeRuntime === "string") {
+      return `vercel-edge,${moduleSource}`;
+    }
+
+    if (navigator?.userAgent === "Cloudflare-Workers") {
+      return `cloudflare-worker,${moduleSource}`;
+    }
+
     return `nodejs,${moduleSource}`;
   } catch {
     // If something unexpectedly occurs, revert to "nodejs".
