├── .husky └── pre-commit ├── .changeset ├── config.json └── README.md ├── .vscode └── settings.json ├── test └── test.ts ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── biome.json ├── LICENSE ├── package.json ├── README.md ├── .gitignore └── src └── index.ts /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run check 2 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": true, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit", 10 | "quickfix.biome": "explicit" 11 | } 12 | } -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { Xvfb } from '../dist/index.js'; 2 | 3 | async function test() { 4 | const xvfb = new Xvfb({ displayNum: 88 }); 5 | 6 | await xvfb.start(); 7 | 8 | console.log('started sync'); 9 | 10 | await xvfb.stop(); 11 | 12 | console.error('stopped sync'); 13 | 14 | await xvfb.start(); 15 | 16 | console.log('started sync'); 17 | 18 | await xvfb.stop(); 19 | 20 | console.error('stopped sync'); 21 | } 22 | 23 | test(); 24 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 1 21 | 22 | - name: Use Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: "20" 26 | 27 | - name: Install dependencies 28 | run: npm install 29 | 30 | - name: Run CI 31 | run: npm run ci 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "verbatimModuleSyntax": true, 12 | /* Strictness */ 13 | "strict": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noImplicitOverride": true, 16 | /* If transpiling with TypeScript: */ 17 | "module": "NodeNext", 18 | "outDir": "dist", 19 | "sourceMap": true, 20 | /* AND if you're building for a library: */ 21 | "declaration": true, 22 | /* If your code doesn't run in the DOM: */ 23 | "lib": ["es2022"] 24 | }, 25 | "exclude": ["node_modules", "dist", "test"] 26 | } -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "enabled": true, 8 | "formatWithErrors": false, 9 | "indentStyle": "space", 10 | "indentWidth": 4, 11 | "lineEnding": "lf", 12 | "lineWidth": 100, 13 | "attributePosition": "auto" 14 | }, 15 | "linter": { 16 | "enabled": true, 17 | "rules": { 18 | "recommended": true 19 | } 20 | }, 21 | "javascript": { 22 | "formatter": { 23 | "jsxQuoteStyle": "double", 24 | "quoteProperties": "asNeeded", 25 | "trailingCommas": "all", 26 | "semicolons": "always", 27 | "arrowParentheses": "always", 28 | "bracketSpacing": true, 29 | "bracketSameLine": false, 30 | "quoteStyle": "single", 31 | "attributePosition": "auto" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rafał Więcek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Rafał Więcek (https://darkgl.pl/)", 3 | "contributors": [ 4 | "ProxV, Inc. (http://proxv.com)", 5 | "Rob Wu (https://robwu.nl)" 6 | ], 7 | "scripts": { 8 | "ci": "npm run build && npm run check && npm run check-exports", 9 | "lint": "npx @biomejs/biome lint --write ./src ./test", 10 | "format": "npx @biomejs/biome format --write ./src ./test", 11 | "check": "npx @biomejs/biome check --write ./src ./test", 12 | "build": "tsc --build", 13 | "prepare": "husky", 14 | "test": "npm run build && node --loader ts-node/esm ./test/test.ts", 15 | "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm", 16 | "local-release": "changeset version && changeset publish", 17 | "prepublishOnly": "npm run ci" 18 | }, 19 | "type": "module", 20 | "name": "xvfb-ts", 21 | "main": "dist/index.js", 22 | "types": "dist/index.d.ts", 23 | "description": "Easily start and stop an X Virtual Frame Buffer from your node app", 24 | "version": "1.1.1", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/DarkGL/node-xvfb-ts.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/DarkGL/node-xvfb-ts/issues" 31 | }, 32 | "homepage": "https://github.com/DarkGL/node-xvfb-ts#readme", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@arethetypeswrong/cli": "^0.18.2", 36 | "@biomejs/biome": "1.9.4", 37 | "@changesets/cli": "^2.29.5", 38 | "@types/node": "^22.10.1", 39 | "husky": "^9.1.7", 40 | "ts-node": "^10.9.2", 41 | "typescript": "^5.9.2" 42 | }, 43 | "keywords": [ 44 | "xvfb", 45 | "framebuffer", 46 | "xvfb-ts", 47 | "xvfb-node", 48 | "headless", 49 | "virtual", 50 | "frame", 51 | "buffer", 52 | "browser", 53 | "chrome", 54 | "firefox", 55 | "safari", 56 | "edge", 57 | "electron", 58 | "puppeteer", 59 | "playwright", 60 | "testing", 61 | "test", 62 | "tests", 63 | "automation", 64 | "selenium", 65 | "webdriver", 66 | "typescript", 67 | "node-xvfb", 68 | "frame buffer" 69 | ], 70 | "files": [ 71 | "dist" 72 | ], 73 | "engines": { 74 | "node": ">=20.17.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-xvfb-ts 2 | 3 | A TypeScript library for easily managing X Virtual Frame Buffer (Xvfb) processes in Node.js applications. Perfect for headless GUI testing with tools like Puppeteer, Playwright, or Selenium. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install xvfb-ts 9 | ``` 10 | 11 | ## Prerequisites 12 | 13 | - Linux/Unix system with Xvfb installed 14 | - Node.js >= 20.17.0 15 | 16 | ## Quick Start 17 | 18 | ```typescript 19 | import { Xvfb } from 'xvfb-ts'; 20 | 21 | const xvfb = new Xvfb(); 22 | 23 | // Start the virtual display 24 | await xvfb.start(); 25 | 26 | // Run your headless GUI tests here 27 | // e.g., launch a browser, run Electron app, etc. 28 | 29 | // Clean up when done 30 | await xvfb.stop(); 31 | ``` 32 | 33 | ## API Reference 34 | 35 | ### Constructor Options 36 | 37 | ```typescript 38 | interface XvfbOptions { 39 | displayNum?: number; // X display number (default: auto-assigned >= 99) 40 | reuse?: boolean; // Reuse existing display (default: false) 41 | timeout?: number; // Startup timeout in ms (default: 500) 42 | silent?: boolean; // Suppress stderr output (default: false) 43 | xvfb_args?: string[]; // Additional Xvfb arguments (default: []) 44 | } 45 | ``` 46 | 47 | ### Methods 48 | 49 | - `start()` - Start the Xvfb process (returns Promise) 50 | - `stop()` - Stop the Xvfb process (returns Promise) 51 | - `display()` - Get the display string (e.g., ":99") 52 | 53 | ## Examples 54 | 55 | ### Basic Usage with Custom Display 56 | 57 | ```typescript 58 | import { Xvfb } from 'xvfb-ts'; 59 | 60 | const xvfb = new Xvfb({ displayNum: 88 }); 61 | 62 | await xvfb.start(); 63 | 64 | console.log(`Display: ${xvfb.display()}`); // :88 65 | 66 | await xvfb.stop(); 67 | ``` 68 | 69 | ### Reusing Existing Display 70 | 71 | ```typescript 72 | const xvfb = new Xvfb({ 73 | displayNum: 99, 74 | reuse: true 75 | }); 76 | 77 | await xvfb.start(); // Won't fail if :99 already exists 78 | ``` 79 | 80 | ### Custom Xvfb Arguments 81 | 82 | ```typescript 83 | const xvfb = new Xvfb({ 84 | xvfb_args: ['-screen', '0', '1024x768x24'] 85 | }); 86 | 87 | await xvfb.start(); 88 | ``` 89 | 90 | ### Error Handling 91 | 92 | ```typescript 93 | try { 94 | await xvfb.start(); 95 | // Your code here 96 | } catch (error) { 97 | console.error('Failed to start Xvfb:', error.message); 98 | } finally { 99 | await xvfb.stop(); 100 | } 101 | ``` 102 | 103 | ## Credits 104 | 105 | * [Rob--W](https://github.com/Rob--W) for the original [node-xvfb](https://github.com/Rob--W/node-xvfb) 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | ### VisualStudioCode ### 145 | .vscode/* 146 | !.vscode/settings.json 147 | !.vscode/tasks.json 148 | !.vscode/launch.json 149 | !.vscode/extensions.json 150 | !.vscode/*.code-snippets 151 | 152 | # Local History for Visual Studio Code 153 | .history/ 154 | 155 | # Built Visual Studio Code Extensions 156 | *.vsix 157 | 158 | ### VisualStudioCode Patch ### 159 | # Ignore all local history of files 160 | .history 161 | .ionide 162 | 163 | # End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode 164 | 165 | node-xvfb.code-workspace -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { type ChildProcess, spawn } from 'node:child_process'; 2 | import fs from 'node:fs'; 3 | 4 | async function sleep(ms: number) { 5 | return new Promise((resolve) => setTimeout(resolve, ms)); 6 | } 7 | 8 | interface XvfbOptions { 9 | displayNum?: number; 10 | reuse?: boolean; 11 | timeout?: number; 12 | silent?: boolean; 13 | xvfb_args?: string[]; 14 | } 15 | 16 | class Xvfb { 17 | private _display: string | null; 18 | private _reuse: boolean; 19 | private _timeout: number; 20 | private _silent: boolean; 21 | private _xvfb_args: string[]; 22 | private _process: ChildProcess | undefined; 23 | private _oldDisplay: string | undefined; 24 | 25 | constructor(options: XvfbOptions = {}) { 26 | this._display = 27 | options.displayNum || options.displayNum === 0 ? `:${options.displayNum}` : null; 28 | this._reuse = options.reuse ?? false; 29 | this._timeout = options.timeout ?? 500; 30 | this._silent = options.silent ?? false; 31 | this._xvfb_args = options.xvfb_args || []; 32 | } 33 | 34 | public async start() { 35 | if (this._process) { 36 | return this._process; 37 | } 38 | 39 | const lockFile = this._lockFile(); 40 | 41 | this._setDisplayEnvVariable(); 42 | 43 | this._spawnProcess(fs.existsSync(lockFile)); 44 | 45 | let totalTime = 0; 46 | 47 | while (!fs.existsSync(lockFile)) { 48 | if (totalTime > this._timeout) { 49 | throw new Error('Could not start Xvfb.'); 50 | } 51 | 52 | await sleep(10); 53 | 54 | totalTime += 10; 55 | } 56 | 57 | return this._process; 58 | } 59 | 60 | public async stop() { 61 | if (!this._process) { 62 | return; 63 | } 64 | 65 | this._killProcess(); 66 | this._restoreDisplayEnvVariable(); 67 | 68 | const lockFile = this._lockFile(); 69 | 70 | let totalTime = 0; 71 | 72 | while (fs.existsSync(lockFile)) { 73 | if (totalTime > this._timeout) { 74 | throw new Error('Could not stop Xvfb.'); 75 | } 76 | 77 | await sleep(10); 78 | 79 | totalTime += 10; 80 | } 81 | } 82 | 83 | public display() { 84 | if (this._display) { 85 | return this._display; 86 | } 87 | 88 | let displayNum = 98; 89 | let lockFile: string | undefined; 90 | 91 | do { 92 | displayNum++; 93 | lockFile = this._lockFile(displayNum); 94 | } while (!this._reuse && fs.existsSync(lockFile)); 95 | 96 | this._display = `:${displayNum}`; 97 | 98 | return this._display; 99 | } 100 | 101 | private _setDisplayEnvVariable() { 102 | this._oldDisplay = process.env.DISPLAY; 103 | 104 | process.env.DISPLAY = this.display(); 105 | } 106 | 107 | private _restoreDisplayEnvVariable() { 108 | process.env.DISPLAY = this._oldDisplay; 109 | } 110 | 111 | private _spawnProcess(lockFileExists: boolean) { 112 | const display = this.display(); 113 | 114 | if (lockFileExists) { 115 | if (!this._reuse) { 116 | throw new Error( 117 | `Display ${display} is already in use and the "reuse" option is false.`, 118 | ); 119 | } 120 | } else { 121 | this._process = spawn('Xvfb', [display].concat(this._xvfb_args)); 122 | 123 | if (!this._process || !this._process.stderr) { 124 | throw new Error('Could not start Xvfb.'); 125 | } 126 | 127 | this._process.stderr.on('data', (data) => { 128 | if (!this._silent) { 129 | process.stderr.write(data); 130 | } 131 | }); 132 | } 133 | } 134 | 135 | private _killProcess() { 136 | if (!this._process) { 137 | return; 138 | } 139 | 140 | this._process.kill(); 141 | 142 | this._process = undefined; 143 | } 144 | 145 | private _lockFile(displayNum?: number) { 146 | return `/tmp/.X${displayNum || this.display().toString().replace(/^:/, '')}-lock`; 147 | } 148 | } 149 | 150 | export { Xvfb }; 151 | --------------------------------------------------------------------------------