Run JavaScript with npm Packages on Any URL
Use a real browser runner for page JavaScript and npm exec for package commands, with runnable local and CI examples.
Direct answer: use a browser runner when your JavaScript needs a real page context such as location, the DOM, cookies, or browser APIs. Use npm exec (or its npx alias) when a package exposes a command that should run against a URL. These are different jobs: a browser runner opens a page and executes code inside it; npm exec resolves and invokes a package command but does not navigate to a URL by itself.
Choose the execution model
| Need | Use | Why |
|---|---|---|
| Read DOM, URL, rendered styles, or browser APIs | browser-run |
Your code runs in a browser page. |
| Run a package’s CLI command | npm exec / npx |
npm resolves the package and passes arguments to its command. |
| Run in Linux CI without a display | Browser runner plus Xvfb | Xvfb supplies a virtual display for headed browser processes. |
| Capture a stable visual result without maintaining a browser | ScreenshotNeo | A hosted screenshot API handles navigation and capture. |
Run page JavaScript with browser-run
browser-run describes itself as “The easiest way of running code in a browser environment.” Its CLI reads JavaScript from standard input, starts Electron by default, streams console output, and can close the browser with window.close().
1. Install it
npm install browser-run
# Or install the command globally
npm install -g browser-run
2. Execute code against a URL
The simplest pattern is to navigate first, then run code after the page loads. The exact navigation mechanism depends on the runner version and your script, so keep page-specific code in the browser context and use the runner’s documented input options.
echo "console.log('Hey from ' + location); window.close()" | browser-run
The command prints browser and page output. Add explicit logging while developing so a failed navigation, missing selector, or script exception is visible in CI logs.
3. Read the DOM
cat <<'JS' | browser-run
const heading = document.querySelector('h1');
if (!heading) {
console.error('No h1 found at', location.href);
window.close();
}
console.log({
url: location.href,
title: document.title,
heading: heading.textContent.trim()
});
window.close();
JS
4. Wait for page code before reading
cat <<'JS' | browser-run
(async () => {
const deadline = Date.now() + 10000;
let app;
while (Date.now() < deadline) {
app = document.querySelector('[data-app-ready="true"]');
if (app) break;
await new Promise(resolve => setTimeout(resolve, 100));
}
if (!app) {
console.error('Timed out waiting for the application');
window.close();
return;
}
console.log(app.textContent.trim());
window.close();
})();
JS
Use npm packages inside browser code
An npm package must be described by a package.json. Modules installed in node_modules can be loaded with require or import; a random JavaScript file is not itself an npm package unless it is part of that package structure. See npm’s documentation on packages and installation.
Local dependency with a browser bundle
Browser page code cannot automatically call arbitrary Node modules. Install a package locally, then use a browser-compatible build or bundle it for the page. For example, a project can install a package and bundle an entry file with its normal build tool:
mkdir url-page-script && cd url-page-script
npm init -y
npm install dayjs
npm install --save-dev esbuild
cat > entry.js <<'JS'
import dayjs from 'dayjs';
console.log('Browser time:', dayjs().toISOString());
JS
npx esbuild entry.js --bundle --format=iife --outfile=browser-entry.js
Serve the generated script from a page or use the runner’s static-asset option. A package that depends on Node-only modules such as fs or native bindings will not work in an ordinary page context without a deliberate Node integration setup.
Run a package command with npm exec
npm documents npm exec -- <pkg>[@<version>] [args...] and npm exec --package=<pkg>[@<version>] -- <cmd>. Package references may be registry names, versions, tags, tarball URLs, or Git URLs. npm resolves the package for the invocation, then runs its exposed command.
Run a pinned package version
npm exec -- playwright@latest --version
For reproducible builds, replace a moving tag with an exact version:
npm exec -- playwright@1.55.0 --version
Use a package command against a URL
If the package’s CLI accepts a URL, pass it after the separator:
npm exec -- some-url-tool@1.2.3 -- https://example.com
The package determines the available flags. Read its own help output before automating:
npm exec -- some-url-tool@1.2.3 -- --help
Make a package available to a command
npm exec --package=typescript@5.9.2 -- tsc --version
This is useful in a clean CI job where you do not want to add a long-lived global install.
Browser context versus Node context
| Capability | Browser page | Node process |
|---|---|---|
document, location, layout |
Available | Unavailable unless a browser library is used |
| Filesystem and child processes | Unavailable by default | Available according to process permissions |
| Cookies and local storage | Page profile and origin rules apply | Must be supplied or managed by a library |
| npm package resolution | Needs a bundled browser-compatible build | Native Node resolution works |
browser-run exposes options for browser selection, sandboxing, static assets, request mocking, Node integration, and a basedir for requiring modules in Node mode. Keep the sandbox enabled unless you have a specific reason to change it. Enabling Node integration changes the security boundary: page code may gain access to local capabilities, so only run trusted pages and scripts.
Automate a URL script in CI
Local, headed execution
set -euo pipefail
cat script.js | browser-run
Linux CI with Xvfb
On a Linux runner without a display, the browser-run project documents using Xvfb. A typical command is:
xvfb-run npm test
Install Xvfb through your CI image or operating-system package manager, then run the browser command under xvfb-run. This is a display workaround; it does not guarantee that every page, browser version, or package behaves identically in CI.
GitHub Actions example
name: inspect-url
on: [push]
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: sudo apt-get update && sudo apt-get install -y xvfb
- run: xvfb-run npm test
Reliable scripts for real sites
- Set a deadline. Stop polling and close the browser after a bounded interval.
- Check the final URL. Redirects, login pages, and consent flows can change what you inspect.
- Check selectors explicitly. Treat a missing element as a useful failure with a clear message.
- Close every run. Call
window.close()on success and failure paths to avoid hanging jobs. - Pin versions in CI. Pin the runner, package, and Node versions when output must be repeatable.
- Keep secrets out of page code. Pass credentials through a secure runtime mechanism and avoid logging them.
Performance, reliability, and cost
- Browser startup is usually the largest fixed cost. Reuse a process for multiple URLs when the runner and isolation requirements allow it.
- Wait for the condition you need instead of using a large fixed sleep. Selector-based waits finish sooner on fast pages and remain safer on slow pages.
- Mock nonessential requests only when the result remains meaningful; mocking can hide failures in third-party scripts.
- Cache npm dependencies in CI, but invalidate the cache when the lockfile or Node version changes.
- Headless CI can differ from a desktop browser because of fonts, GPU support, viewport size, timing, and sandbox permissions. Record those settings with each result.
npm execmay resolve a package at invocation time. Pin versions for reproducibility and review package scripts before running them.- There is no universal cost or speed guarantee for a package or browser runner; measure your page mix, startup frequency, and timeout policy.
Common errors and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
browser-run: command not found |
Only a local install exists, or the global bin directory is not on PATH. |
Run through the project install with npx browser-run, or fix the global npm bin path. |
document is not defined |
Code is running in Node, not a browser page. | Pipe it to a browser runner or use a browser automation library. |
| Package import fails in the page | The dependency is Node-only or was not bundled. | Bundle a browser-compatible entry point, or move that operation to Node. |
| Browser cannot start in CI | No display, missing browser dependency, or sandbox restriction. | Use Xvfb for display-less Linux, install required dependencies, and inspect the runner’s browser and sandbox options. |
| Script exits before dynamic content appears | It reads the DOM immediately. | Poll for a specific selector with a timeout and report a useful failure. |
| Command runs the wrong package | Unpinned package name, ambiguous binary, or argument placement. | Pin the version, use --package, and put command arguments after --. |
| Run hangs | The browser was never closed or a request remained pending. | Use a deadline, close the window in all paths, and add request or page diagnostics. |
| Unexpected login or consent page | The URL requires state, authentication, or interaction. | Provide the required test state securely, handle the flow explicitly, and verify the final URL before extracting data. |
Or skip the browser setup
For screenshot output, ScreenshotNeo provides one GET request that returns PNG, JPEG, WebP, or PDF. The API accepts the URL and handles the capture service for you. See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can npm exec open a URL by itself?
No. npm exec invokes a package command. The package must implement URL handling, or you must combine it with a browser runner or another HTTP/browser library.
Can every npm package run in a browser?
No. Packages that require Node APIs, native modules, or filesystem access need a Node process or a deliberate integration layer. Browser-compatible packages can be bundled for page execution.
Is Xvfb a browser?
No. Xvfb supplies a virtual display so a browser process can run on a headless Linux machine.
Should I enable Node integration?
Only for trusted content and when page code must access Node modules. It changes the security model and should be treated as a privileged mode.
How do I make runs reproducible?
Pin Node, browser-run, package, and package versions; lock dependencies; record viewport and environment settings; and use explicit waits and timeouts.


