or run

npx @tessl/cli init
Log in

Version

Files

docs

api

cookie-management.mdjsdom-constructor.mdresource-loading.mdvirtual-console.mdwindow-dom-apis.md
index.md
tile.json

resource-loading.mddocs/api/

Resource Loading

The ResourceLoader class handles loading of external resources including scripts, stylesheets, images, and iframes. It can be customized via subclassing for advanced use cases like proxies, custom headers, or response modification.

Capabilities

Resource Loading Basics

By default, jsdom does not load any external resources. Enable resource loading with the resources option.

Usage Examples:

const { JSDOM } = require("jsdom");

// Default: No resources loaded
const dom1 = new JSDOM(`
  <script src="https://example.com/script.js"></script>
  <link rel="stylesheet" href="https://example.com/style.css">
`);
// script.js and style.css will NOT be loaded

// Enable usable resources
const dom2 = new JSDOM(`
  <script src="https://example.com/script.js"></script>
  <link rel="stylesheet" href="https://example.com/style.css">
`, {
  resources: "usable",
  runScripts: "dangerously" // Required for external scripts
});
// script.js and style.css WILL be loaded

Usable Resources

When resources: "usable" is set, jsdom loads the following resource types:

  • Frames and iframes: <frame> and <iframe> elements
  • Stylesheets: <link rel="stylesheet"> elements
  • Scripts: <script src=""> elements (requires runScripts: "dangerously")
  • Images: <img> elements (requires optional canvas npm package)

Important: Set a valid url option to resolve relative URLs correctly. The default URL is "about:blank", which causes relative URLs to fail.

Usage Example:

const { JSDOM } = require("jsdom");

// BAD: Relative URLs will fail with default "about:blank"
const dom1 = new JSDOM(`
  <script src="/script.js"></script>
`, {
  resources: "usable",
  runScripts: "dangerously"
});
// Fails: Cannot parse "/script.js" against "about:blank"

// GOOD: Provide base URL for relative URLs
const dom2 = new JSDOM(`
  <script src="/script.js"></script>
`, {
  url: "https://example.org/",
  resources: "usable",
  runScripts: "dangerously"
});
// Success: Loads "https://example.org/script.js"

// Or use fromURL() which sets URL automatically
JSDOM.fromURL("https://example.org/page.html", {
  resources: "usable",
  runScripts: "dangerously"
}).then(dom => {
  // All resources are loaded with correct base URL
});

ResourceLoader Constructor

Creates a custom resource loader with configuration for proxies, SSL, and user agent.

/**
 * Creates a resource loader
 * @param options - Configuration options
 */
constructor(options?: ResourceLoaderOptions);

interface ResourceLoaderOptions {
  /**
   * HTTP proxy address
   * @example "http://127.0.0.1:9001"
   */
  proxy?: string;

  /**
   * Whether to require valid SSL certificates
   * @default true
   */
  strictSSL?: boolean;

  /**
   * User-Agent header for requests
   * Also sets navigator.userAgent
   * @default "Mozilla/5.0 (${process.platform}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/${version}"
   */
  userAgent?: string;
}

Usage Example:

const { JSDOM, ResourceLoader } = require("jsdom");

// Basic configuration
const resourceLoader = new ResourceLoader({
  proxy: "http://127.0.0.1:9001",
  strictSSL: false,
  userAgent: "MyApp/1.0"
});

const dom = new JSDOM(`
  <html>
    <head>
      <script src="https://example.com/script.js"></script>
      <link rel="stylesheet" href="https://example.com/style.css">
    </head>
  </html>
`, {
  url: "https://example.org/",
  resources: resourceLoader,
  runScripts: "dangerously"
});

// All requests will:
// - Use the proxy
// - Accept invalid SSL certificates
// - Send "User-Agent: MyApp/1.0" header

console.log(dom.window.navigator.userAgent); // "MyApp/1.0"

Custom Resource Loading

Subclass ResourceLoader and override the fetch() method for complete control over resource loading.

/**
 * Fetch a resource
 * Override this method in subclasses for custom behavior
 * @param url - URL to fetch
 * @param options - Request options
 * @returns Promise resolving to Buffer of content, or null to skip loading.
 *          For HTTP/HTTPS requests, the Promise has additional properties:
 *          - response: HTTP response object (available after 'response' event)
 *          - href: Final URL after redirects (available after request completes)
 *          - abort(): Method to cancel the request
 *          - getHeader(name): Method to retrieve request header values
 */
fetch(url: string, options: FetchOptions): FetchPromise;

interface FetchOptions {
  /**
   * DOM element requesting the resource (if applicable)
   */
  element?: Element;

  /**
   * Accept header value
   */
  accept?: string;

  /**
   * Cookie jar for sending cookies
   */
  cookieJar?: CookieJar;

  /**
   * Referrer URL
   */
  referrer?: string;
}

interface FetchPromise extends Promise<Buffer | null> {
  /**
   * HTTP response object (only for HTTP/HTTPS requests)
   * Set during the 'response' event
   */
  response?: {
    statusCode: number;
    headers: Record<string, string>;
  };

  /**
   * Final URL after following redirects (only for HTTP/HTTPS requests)
   * Available after request completes
   */
  href?: string;

  /**
   * Abort the fetch request (all request types)
   */
  abort(): void;

  /**
   * Get a request header value (only for HTTP/HTTPS requests)
   * @param name - Header name
   * @returns Header value
   */
  getHeader?(name: string): string | undefined;
}

Usage Example:

const { JSDOM, ResourceLoader } = require("jsdom");

// Override specific URL
class CustomResourceLoader extends ResourceLoader {
  fetch(url, options) {
    // Override a specific script's contents
    if (url === "https://example.com/config.js") {
      return Promise.resolve(Buffer.from(`
        window.CONFIG = { apiUrl: "https://test-api.example.com" };
      `));
    }

    // Block certain resources
    if (url.includes("analytics")) {
      return Promise.resolve(null); // Return null to skip loading
    }

    // Use default behavior for everything else
    return super.fetch(url, options);
  }
}

const dom = new JSDOM(`
  <html>
    <head>
      <script src="https://example.com/config.js"></script>
      <script src="https://analytics.example.com/tracker.js"></script>
      <script src="https://example.com/app.js"></script>
    </head>
  </html>
`, {
  url: "https://example.org/",
  resources: new CustomResourceLoader(),
  runScripts: "dangerously"
});

// config.js loads with custom content
// tracker.js is blocked
// app.js loads normally

Logging Resource Requests

Use the element option to identify which element is requesting a resource.

Usage Example:

const { JSDOM, ResourceLoader } = require("jsdom");

class LoggingResourceLoader extends ResourceLoader {
  fetch(url, options) {
    if (options.element) {
      console.log(`Element ${options.element.localName} is requesting ${url}`);
      console.log(`Element attributes:`, {
        id: options.element.id,
        className: options.element.className,
        src: options.element.src,
        href: options.element.href
      });
    } else {
      console.log(`Requesting ${url} (no element)`);
    }

    return super.fetch(url, options);
  }
}

const dom = new JSDOM(`
  <html>
    <head>
      <link rel="stylesheet" href="https://example.com/style.css">
      <script src="https://example.com/script.js"></script>
    </head>
    <body>
      <img id="logo" src="https://example.com/logo.png">
      <iframe src="https://example.com/widget.html"></iframe>
    </body>
  </html>
`, {
  url: "https://example.org/",
  resources: new LoggingResourceLoader(),
  runScripts: "dangerously"
});

// Output:
// Element link is requesting https://example.com/style.css
// Element script is requesting https://example.com/script.js
// Element img is requesting https://example.com/logo.png
// Element iframe is requesting https://example.com/widget.html

Modifying Resources

Intercept and modify resource contents before they're used.

Usage Example:

const { JSDOM, ResourceLoader } = require("jsdom");

class ModifyingResourceLoader extends ResourceLoader {
  async fetch(url, options) {
    const buffer = await super.fetch(url, options);

    if (!buffer) return null;

    // Modify JavaScript files
    if (url.endsWith(".js")) {
      let content = buffer.toString("utf-8");

      // Add instrumentation
      content = `console.log("Loading ${url}");\n` + content;

      // Replace API endpoints
      content = content.replace(
        /https:\/\/api\.example\.com/g,
        "https://test-api.example.com"
      );

      return Buffer.from(content, "utf-8");
    }

    // Modify CSS files
    if (url.endsWith(".css")) {
      let content = buffer.toString("utf-8");

      // Add custom CSS
      content = `/* Custom styles */\n` + content;

      return Buffer.from(content, "utf-8");
    }

    return buffer;
  }
}

const dom = new JSDOM(html, {
  url: "https://example.org/",
  resources: new ModifyingResourceLoader(),
  runScripts: "dangerously"
});

Caching Resources

Implement a cache to avoid re-fetching resources.

Usage Example:

const { JSDOM, ResourceLoader } = require("jsdom");

class CachingResourceLoader extends ResourceLoader {
  constructor(options) {
    super(options);
    this.cache = new Map();
  }

  async fetch(url, options) {
    // Check cache
    if (this.cache.has(url)) {
      console.log(`Cache hit: ${url}`);
      return this.cache.get(url);
    }

    console.log(`Cache miss: ${url}`);

    // Fetch and cache
    const buffer = await super.fetch(url, options);

    if (buffer) {
      this.cache.set(url, buffer);
    }

    return buffer;
  }

  clearCache() {
    this.cache.clear();
  }
}

const loader = new CachingResourceLoader();

// First page - fetches resources
const dom1 = await JSDOM.fromURL("https://example.com/", {
  resources: loader,
  runScripts: "dangerously"
});

// Second page - uses cached resources (if URLs match)
const dom2 = await JSDOM.fromURL("https://example.com/page2", {
  resources: loader,
  runScripts: "dangerously"
});

// Clear cache when done
loader.clearCache();

Async Resource Loading

Note that resource loading is asynchronous. Scripts and stylesheets may not be loaded immediately.

Usage Example:

const { JSDOM } = require("jsdom");

const dom = new JSDOM(`
  <html>
    <head>
      <script src="https://example.com/slow-loading.js"></script>
    </head>
    <body>
      <div id="content"></div>
      <script>
        // This inline script runs immediately
        // But slow-loading.js may not have loaded yet
        console.log("Inline script executed");
      </script>
    </body>
  </html>
`, {
  url: "https://example.org/",
  resources: "usable",
  runScripts: "dangerously"
});

// The page structure is available immediately
console.log(dom.window.document.getElementById("content")); // <div>

// But external scripts may still be loading
// There's no built-in way to know when all resources finish loading

// Workaround: Use a callback in the page
const domWithCallback = new JSDOM(`
  <html>
    <head>
      <script src="https://example.com/lib.js"></script>
    </head>
    <body>
      <script>
        // Signal when ready
        if (typeof window.onLibraryLoaded === 'function') {
          window.onLibraryLoaded();
        }
      </script>
    </body>
  </html>
`, {
  url: "https://example.org/",
  resources: "usable",
  runScripts: "dangerously"
});

// Set up callback before or during creation
domWithCallback.window.onLibraryLoaded = () => {
  console.log("Library is loaded!");
};

Error Handling

Resource loading errors are reported via the virtual console's jsdomError event.

Usage Example:

const { JSDOM, VirtualConsole } = require("jsdom");

const virtualConsole = new VirtualConsole();

virtualConsole.on("jsdomError", (error) => {
  if (error.type === "resource-loading") {
    console.error(`Failed to load: ${error.url}`);
    console.error(`Error: ${error.cause?.message}`);
  }
});

const dom = new JSDOM(`
  <html>
    <head>
      <script src="https://nonexistent.example.com/script.js"></script>
      <link rel="stylesheet" href="https://example.com/missing.css">
    </head>
  </html>
`, {
  url: "https://example.org/",
  resources: "usable",
  runScripts: "dangerously",
  virtualConsole
});

// Will emit jsdomError events for failed resources

Synchronous Resource Access

For resources you need synchronously, provide them via custom ResourceLoader or beforeParse hook.

Usage Example:

const { JSDOM, ResourceLoader } = require("jsdom");
const fs = require("fs");

class LocalResourceLoader extends ResourceLoader {
  fetch(url, options) {
    // Serve local files for specific URLs
    if (url === "https://example.com/jquery.js") {
      const content = fs.readFileSync("./node_modules/jquery/dist/jquery.js");
      return Promise.resolve(content);
    }

    return super.fetch(url, options);
  }
}

const dom = new JSDOM(`
  <html>
    <head>
      <script src="https://example.com/jquery.js"></script>
    </head>
  </html>
`, {
  url: "https://example.org/",
  resources: new LocalResourceLoader(),
  runScripts: "dangerously"
});

// Or inject directly in beforeParse
const dom2 = new JSDOM(`
  <html>
    <body>
      <script>
        // jQuery is already available
        $('body').append('<p>Hello</p>');
      </script>
    </body>
  </html>
`, {
  runScripts: "dangerously",
  beforeParse(window) {
    // Inject library before parsing
    const jqueryCode = fs.readFileSync("./node_modules/jquery/dist/jquery.js", "utf-8");
    window.eval(jqueryCode);
  }
});

Resource Loading and Cookies

Cookies from the cookie jar are automatically sent with resource requests.

Usage Example:

const { JSDOM, CookieJar } = require("jsdom");

const cookieJar = new CookieJar();
cookieJar.setCookieSync("auth=token123", "https://example.com/");

const dom = new JSDOM(`
  <html>
    <head>
      <script src="https://example.com/authenticated-api.js"></script>
    </head>
  </html>
`, {
  url: "https://example.com/",
  resources: "usable",
  runScripts: "dangerously",
  cookieJar
});

// The request for authenticated-api.js will include:
// Cookie: auth=token123