Node Lynx
Use node-lynx as a local Lynx runtime for screenshots, preview windows, smoke tests, DebugRouter OpenCard sessions, and automation that needs to inspect or interact with a rendered Lynx page from Node.js.
Default sample template URL:
https://lynxjs.org/lynx-examples/gallery/dist/GalleryComplete.lynx.bundle
CLI
Prefer the CLI for one-off screenshots, quick template validation, and simple preview sessions.
Show available options:
node-lynx --help
Capture a headless screenshot and exit:
node-lynx render \
"https://lynxjs.org/lynx-examples/gallery/dist/GalleryComplete.lynx.bundle" \
--width 268 \
--height 469 \
--dpr 2 \
--output ./gallery.png \
--timeout 30000 \
--screenshot-delay 500 \
--no-debug-router
Render a local bundle:
node-lynx render \
--template ./dist/main/template.js \
--width 390 \
--height 844 \
--output ./node-lynx-local.png \
--no-debug-router
Open a visible preview window on macOS:
node-lynx preview \
"https://lynxjs.org/lynx-examples/gallery/dist/GalleryComplete.lynx.bundle" \
--width 268 \
--height 469 \
--dpr 2 \
--title "Node Lynx Preview"
Start a DebugRouter-backed session without an initial template:
node-lynx render
CLI rules:
- Use
renderfor headless screenshots and automation. - Use
previewonly on macOS; it creates an AppKit window. - Use
--template <path>for local bundles and--url <url>for remote bundles. A positionalhttp://orhttps://input is treated as a remote bundle URL. - Use
--widthand--heightas CSS pixel viewport values. - Use
--dpror--device-pixel-ratioto control output scale; the PNG pixel size iswidth * dprbyheight * dpr. - Use
--timeout <ms>for bundle download, template load, CDP calls, and first frame submission timeouts. - Use
--screenshot-delay <ms>to wait after the first frame before capture. This defaults to100; use a larger delay for image-heavy or network-heavy pages. - Pass
--no-debug-routerfor CI or one-shot screenshot commands that should exit immediately after writing the PNG. - Omit
--no-debug-routerwhen you need DebugRouter/OpenCard. Without an initial template,renderwaits for DebugRouter OpenCard.previewalso waits for OpenCard without creating an initial window.
TypeScript API
Prefer the API when the task needs programmatic setup, repeated captures, global props, CDP calls, input simulation, or custom lifecycle handling.
Import from the node-lynx package declared by the target project. For public npm usage this is typically:
import { HeadlessLynxView } from '@lynx-js/node-lynx';
Render a remote template to PNG:
import { writeFile } from 'node:fs/promises';
import { HeadlessLynxView } from '@lynx-js/node-lynx';
const templateUrl =
'https://lynxjs.org/lynx-examples/gallery/dist/GalleryComplete.lynx.bundle';
const view = new HeadlessLynxView({
width: 268,
height: 469,
devicePixelRatio: 2,
timeoutMs: 30000,
});
try {
await view.loadTemplateFromUrl(templateUrl, {
initialData: { source: 'node-lynx' },
globalProps: { theme: 'light' },
});
const png = await view.screenshot({ settleMs: 500 });
await writeFile('./gallery.png', png);
} finally {
view.destroy();
}
Render a local bundle buffer. Always pass a file URL so relative resources have the right base URL:
import { readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { HeadlessLynxView } from '@lynx-js/node-lynx';
const templatePath = resolve('./dist/main/template.js');
const view = new HeadlessLynxView({ width: 390, height: 844 });
try {
await view.loadTemplate(await readFile(templatePath), {
url: pathToFileURL(templatePath).href,
});
await writeFile('./node-lynx-local.png', await view.screenshot());
} finally {
view.destroy();
}
Inspect the DOM through CDP:
const responseText = await view.invokeCDPFromSDK(
JSON.stringify({
id: 1,
method: 'DOM.getDocument',
params: { depth: -1, pierce: true },
})
);
const response = JSON.parse(responseText);
Update runtime data after load:
view.updateData({ selected: true });
view.updateGlobalProps({ locale: 'en-US' });
await view.waitForFrame();
Windowed API
Use WindowedLynxView only on macOS when a visible app window is required.
import { WindowedLynxView } from '@lynx-js/node-lynx';
const view = new WindowedLynxView({
width: 268,
height: 469,
devicePixelRatio: 2,
title: 'Node Lynx Preview',
});
try {
await view.loadTemplateFromUrl(
'https://lynxjs.org/lynx-examples/gallery/dist/GalleryComplete.lynx.bundle'
);
await view.waitForFrame();
view.click(20, 70);
view.typeText('hello from node-lynx');
view.pressKey('Enter');
await view.waitUntilClosed();
} finally {
view.destroy();
}
Interaction APIs use CSS pixel coordinates. Supported pressKey values are
Backspace, Delete, Enter, ArrowLeft, ArrowRight, ArrowUp, and
ArrowDown.
DebugRouter OpenCard
Use OpenCard managers when the page should be opened by DebugRouter instead of an initial CLI/API template load.
import {
HeadlessOpenCardManager,
LynxEnv,
WindowedOpenCardManager,
} from '@lynx-js/node-lynx';
LynxEnv.init();
LynxEnv.setAppInfo(['App', 'AppVersion'], ['NodeLynxSkill', '1.0.0']);
const Manager =
process.platform === 'darwin' ? WindowedOpenCardManager : HeadlessOpenCardManager;
const manager = new Manager({
view: { width: 268, height: 469, devicePixelRatio: 2, timeoutMs: 30000 },
onCardLoaded(card) {
console.log(`opened ${card.url}`);
},
onCardError(error, card) {
console.error(`failed ${card.url}: ${error.message}`);
},
});
manager.install();
process.once('SIGINT', () => {
manager.dispose();
process.exit(0);
});
Operational Notes
- Prefer a real remote template URL when the bundle loads URL-relative resources.
- Treat
waitForFrame()as "a frame was submitted", not as proof that every image or network resource has decoded. - Use
screenshot({ settleMs })or CLI--screenshot-delayfor pragmatic post-frame settling. Prefer a page-level ready signal when the page exposes one. - Always call
destroy()in afinallyblock for each view. - Keep examples and troubleshooting scoped to node-lynx usage. Do not add internal package names, internal registries, or private distribution details to this open-source-facing skill.