import { JSDOM, VirtualConsole } from "jsdom";
import { CookieJar } from "tough-cookie";
import fetch from "node-fetch";
export type BrowserCookie = {
name: string;
value: string;
domain?: string;
path?: string;
expires?: number;
secure?: boolean;
httpOnly?: boolean;
sameSite?: "Strict" | "Lax" | "None" | string;
};
function buildCookieJar(cookies: BrowserCookie[], defaultDomain: string): CookieJar {
const jar = new CookieJar();
const origin = `https://${defaultDomain}`;
for (const cookie of cookies) {
const parts: string[] = [];
parts.push(`${cookie.name}=${cookie.value}`);
parts.push(`Domain=${cookie.domain ?? defaultDomain}`);
parts.push(`Path=${cookie.path ?? "/"}`);
if (cookie.secure) parts.push("Secure");
if (cookie.httpOnly) parts.push("HttpOnly");
if (cookie.sameSite) parts.push(`SameSite=${cookie.sameSite}`);
if (cookie.expires && Number.isFinite(cookie.expires)) {
const date = new Date(cookie.expires * 1000);
parts.push(`Expires=${date.toUTCString()}`);
}
const cookieString = parts.join("; ");
try {
jar.setCookieSync(cookieString, origin);
} catch {
// best-effort: skip malformed cookies
}
}
return jar;
}
export async function openWithCookies(
url: string,
cookies: BrowserCookie[],
options?: {
userAgent?: string;
domain?: string;
enableScripts?: boolean;
consolePrefix?: string;
timeoutMs?: number;
},
) {
const domain = options?.domain ?? "www.realcanadiansuperstore.ca";
const jar = buildCookieJar(cookies, domain);
const vConsole = new VirtualConsole();
const prefix = options?.consolePrefix ?? "[jsdom]";
vConsole.on("error", (e) => console.error(`${prefix} error:`, e));
vConsole.on("warn", (e) => console.warn(`${prefix} warn:`, e));
vConsole.on("info", (e) => console.info(`${prefix} info:`, e));
vConsole.on("log", (e) => console.log(`${prefix} log:`, e));
const dom = await JSDOM.fromURL(url, {
resources: "usable",
runScripts: options?.enableScripts ? "dangerously" : "outside-only",
pretendToBeVisual: true,
cookieJar: jar,
userAgent: options?.userAgent,
virtualConsole: vConsole,
referrer: `https://${domain}/account`,
beforeParse(window) {
// Minimal navigator/matchMedia to satisfy client checks
(window as any).navigator = (window as any).navigator || {};
if (!(window as any).matchMedia) {
(window as any).matchMedia = () => ({ matches: false, addListener() {}, removeListener() {} } as any);
}
// Polyfill fetch to include cookies from jar
(window as any).fetch = async (input: any, init?: any) => {
const base = `https://${domain}`;
const target = new URL(typeof input === 'string' ? input : input?.toString?.() ?? '', base).toString();
const cookie = await new Promise<string>((resolve) => {
jar.getCookieString(target, {}, (_err, str) => resolve(str || ''));
});
const headers = Object.assign({}, init?.headers || {}, cookie ? { Cookie: cookie } : {});
const res = await fetch(target, { ...init, headers } as any);
return res as any;
};
},
// Keep default contentType; site serves proper headers
});
return dom;
}
export async function waitForSelector(
document: Document,
selector: string,
timeoutMs: number = 10000,
pollMs: number = 200,
): Promise<Element | null> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await new Promise((r) => setTimeout(r, pollMs));
}
return null;
}