├── .editorconfig ├── .eslintrc.yaml ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmrc ├── LICENSE ├── Makefile ├── README.md ├── child.ts ├── index.test.ts ├── index.ts ├── package-lock.json ├── package.json ├── tsconfig.json ├── updates.config.js └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - silverwind 4 | - silverwind-typescript 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.snap linguist-language=JavaScript 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{matrix.os}} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: latest 16 | - uses: oven-sh/setup-bun@v1 17 | with: 18 | bun-version: latest 19 | - run: make lint test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /dist 3 | /node_modules 4 | /npm-debug.log* 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | audit=false 2 | fund=false 3 | package-lock=true 4 | save-exact=true 5 | update-notifier=false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) silverwind 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE_FILES := index.ts 2 | DIST_FILES := dist/index.js 3 | 4 | node_modules: package-lock.json 5 | npm install --no-save 6 | @touch node_modules 7 | 8 | .PHONY: deps 9 | deps: node_modules 10 | 11 | .PHONY: lint 12 | lint: node_modules 13 | npx eslint --ext js,jsx,ts,tsx --color . 14 | npx tsc 15 | 16 | .PHONY: lint-fix 17 | lint-fix: node_modules 18 | npx eslint --ext js,jsx,ts,tsx --color . --fix 19 | npx tsc 20 | 21 | .PHONY: test 22 | test: node_modules 23 | bun test 24 | 25 | .PHONY: test-update 26 | test-update: node_modules 27 | bun test 28 | 29 | .PHONY: build 30 | build: node_modules $(DIST_FILES) 31 | 32 | $(DIST_FILES): $(SOURCE_FILES) package-lock.json vite.config.ts 33 | npx vite build 34 | 35 | .PHONY: publish 36 | publish: node_modules 37 | git push -u --tags origin master 38 | npm publish 39 | 40 | .PHONY: update 41 | update: node_modules 42 | npx updates -cu 43 | rm -rf node_modules package-lock.json 44 | npm install 45 | @touch node_modules 46 | 47 | .PHONY: path 48 | patch: node_modules lint test build 49 | npx versions patch package.json package-lock.json 50 | @$(MAKE) --no-print-directory publish 51 | 52 | .PHONY: minor 53 | minor: node_modules lint test build 54 | npx versions minor package.json package-lock.json 55 | @$(MAKE) --no-print-directory publish 56 | 57 | .PHONY: major 58 | major: node_modules lint test build 59 | npx versions major package.json package-lock.json 60 | @$(MAKE) --no-print-directory publish 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # daemonize-process 2 | [![](https://img.shields.io/npm/v/daemonize-process.svg?style=flat)](https://www.npmjs.org/package/daemonize-process) [![](https://img.shields.io/npm/dm/daemonize-process.svg)](https://www.npmjs.org/package/daemonize-process) [![](https://packagephobia.com/badge?p=daemonize-process)](https://packagephobia.com/result?p=daemonize-process) 3 | 4 | > Daemonize the current Node.js process 5 | 6 | The module re-spawns the current process and then exits, which will lead to the new child process being adopted by `init` or similar mechanisms, effectively putting the current process into the background. 7 | 8 | Differences to the `daemon` module include: 9 | 10 | - Exposes all options of `child_process.spawn`. 11 | - Cleans up `process.env` after itself. 12 | 13 | ## Install 14 | 15 | ```console 16 | $ npm i daemonize-process 17 | ``` 18 | 19 | ## Examples 20 | 21 | ```js 22 | import {daemonizeProcess} from "daemonize-process"; 23 | 24 | daemonizeProcess(); 25 | 26 | // below here the process is daemonized 27 | ``` 28 | 29 | ## API 30 | 31 | ### daemonizeProcess([options]) 32 | 33 | The `options` object can contain any valid [`child_process.spawn` option](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) as well as these properties: 34 | 35 | - `script` *string* - The path to the script to be executed. Default: The current script. 36 | - `arguments` *Array* - The command line arguments to be used. Default: The current arguments. 37 | - `node` *string* - The path to the Node.js binary to be used. Default: The current Node.js binary. 38 | - `exitCode` *number* - The exit code to be used when exiting the parent process. Default: `0`. 39 | 40 | By default the standard streams of the child are ignored (e.g. attached to `/dev/null` or equivalent). If you need these streams, adjust the `stdio` option. 41 | 42 | ## License 43 | 44 | © [silverwind](https://github.com/silverwind), distributed under BSD licence 45 | -------------------------------------------------------------------------------- /child.ts: -------------------------------------------------------------------------------- 1 | import {daemonizeProcess} from "./index.ts"; 2 | import {writeFileSync} from "node:fs"; 3 | import {env, ppid} from "node:process"; 4 | 5 | daemonizeProcess(); 6 | const output = [String(ppid), "_DAEMONIZE_PROCESS" in env].join(","); 7 | writeFileSync(new URL("test-output", import.meta.url), output); 8 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import {fork} from "node:child_process"; 2 | import {platform, tmpdir} from "node:os"; 3 | import {mkdtempSync} from "node:fs"; 4 | import {readFile, copyFile, rm} from "node:fs/promises"; 5 | import {join} from "node:path"; 6 | 7 | const testDir = mkdtempSync(join(tmpdir(), "daemonize-process-")); 8 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 9 | 10 | beforeAll(async () => { 11 | await copyFile(new URL("index.ts", import.meta.url), join(testDir, "index.ts")); 12 | await copyFile(new URL("child.ts", import.meta.url), join(testDir, "child.ts")); 13 | await copyFile(new URL("package.json", import.meta.url), join(testDir, "package.json")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await rm(testDir, {recursive: true}); 18 | }); 19 | 20 | test("simple", () => { 21 | return new Promise(resolve => { 22 | const child = fork(join(testDir, "child.ts")); 23 | 24 | child.on("exit", async () => { 25 | await sleep(1000); // give the child some time to exit 26 | const [ppid, envVar] = (await readFile(join(testDir, "test-output"), "utf8")).split(","); 27 | 28 | // check if process was correctly orphaned. on unix, this should generally mean that 29 | // pid 1 picked up the child, but it'll be different on other platforms. 30 | if (platform() === "win32") { 31 | expect(ppid).toMatch(/[0-9]+/); 32 | } else { 33 | expect(["0", "1"].includes(ppid)).toEqual(true); 34 | } 35 | 36 | // verify that internal tracking variable is not leaked to the child 37 | expect(envVar).toEqual("false"); 38 | resolve(undefined); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import {spawn} from "node:child_process"; 2 | import {env, cwd, execPath, argv, exit} from "node:process"; 3 | import type {SpawnOptions} from "node:child_process"; 4 | 5 | const id = "_DAEMONIZE_PROCESS"; 6 | 7 | type DaemonizeProcessOpts = { 8 | /** The path to the script to be executed. Default: The current script. */ 9 | script?: string, 10 | /** The command line arguments to be used. Default: The current arguments. */ 11 | arguments?: string[], 12 | /** The path to the Node.js binary to be used. Default: The current Node.js binary. */ 13 | node?: string, 14 | /** The exit code to be used when exiting the parent process. Default: `0`. */ 15 | exitCode?: number, 16 | } & SpawnOptions; 17 | 18 | export function daemonizeProcess(opts: DaemonizeProcessOpts = {}) { 19 | if (id in env) { 20 | // In the child, clean up the tracking environment variable 21 | delete env[id]; 22 | } else { 23 | // In the parent, set the tracking environment variable, fork the child and exit 24 | const o: DaemonizeProcessOpts = { 25 | // spawn options 26 | env: Object.assign(env, opts.env, {[id]: "1"}), 27 | cwd: cwd(), 28 | stdio: "ignore", 29 | detached: true, 30 | // custom options 31 | node: execPath, 32 | script: argv[1], 33 | arguments: argv.slice(2), 34 | exitCode: 0, 35 | ...opts, 36 | }; 37 | 38 | const args: string[] = [o.script as string, ...(o.arguments as string[])]; 39 | const proc: any = spawn(o.node as string, args, o); 40 | proc?.unref?.(); 41 | exit(o.exitCode); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daemonize-process", 3 | "version": "4.1.1", 4 | "description": "Daemonize the current Node.js process", 5 | "author": "silverwind ", 6 | "repository": "silverwind/daemonize-process", 7 | "license": "BSD-2-Clause", 8 | "type": "module", 9 | "sideEffects": false, 10 | "main": "./dist/index.js", 11 | "exports": "./dist/index.js", 12 | "types": "./dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "22.13.4", 21 | "eslint": "8.57.0", 22 | "eslint-config-silverwind": "99.0.0", 23 | "eslint-config-silverwind-typescript": "9.2.2", 24 | "jest-extended": "4.0.2", 25 | "typescript": "5.7.3", 26 | "typescript-config-silverwind": "7.0.0", 27 | "updates": "16.4.2", 28 | "versions": "12.1.3", 29 | "vite": "6.1.0", 30 | "vite-config-silverwind": "4.0.0", 31 | "vitest": "3.0.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "typescript-config-silverwind", 3 | "compilerOptions": { 4 | "strict": true, 5 | "types": [ 6 | "jest-extended", 7 | "vite/client", 8 | "vitest/globals", 9 | ], 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /updates.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | exclude: [ 3 | "eslint", 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vite"; 2 | import {nodeLib} from "vite-config-silverwind"; 3 | 4 | export default defineConfig(nodeLib({ 5 | url: import.meta.url, 6 | dtsExcludes: ["child.ts"], 7 | build: { 8 | target: "node18", 9 | } 10 | })); 11 | --------------------------------------------------------------------------------