こういう JavaScript ファイルと ShellScript ファイルがあります

[job.sh]
#!/bin/bash

echo pre something something

./job.js &

echo post something something

[job.js]
#!/usr/bin/env node

console.log("Start job.js")

setTimeout(() => {
console.log("initialized")
}, 500)

// keep process
setInterval(() => {}, 100000)

job.js は常駐するプロセスです
ここでは setInterval でプロセスが終了しないようにしています
これを job.sh から起動します
job.sh は別の Node.js プロセスから起動します
job.sh を起動する側はこんな感じです

const cp = require("child_process")

const sub = cp.spawn("./job.sh")

sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})

job.sh や job.js の出力を受け取りたいので stdout の標準出力にリスナをつけています
job.js は常駐しますがすべてを受け取る必要はなくて 起動直後の内容だけでいいです
つまりは job.sh が終われば close イベントが起きてほしいです
しかし close イベントは起きません
子プロセスである job.sh は終了済みなのですが孫プロセスの job.js が生きていることで stdout や stderr が生きてるのでこれらが閉じられるまでは Node.js は子プロセスが終了したとみなしてくれないようです
放置でもいい気はしますがずっと残るわけなのでメモリリーク状態です

stdio が問題なので ignore にしてしまえば標準入出力を使わないようにできて これなら即終了とみなされ CLOSED が表示されます

const sub = cp.spawn("./job.sh", { stdio: "ignore" })

ただし問題があって標準出力を受け取れないです
ファイルに出力するようにして ファイルを読み取るということはできますが 回りくどくてイマイチです

起動する側もサーバーみたいな常駐するものだとメモリリークになるのを気にしないとですけど 一回限りのスクリプトでスクリプトが終了しないのが嫌という場合は unref で解決できます

const cp = require("child_process")

sub = cp.spawn("./job.sh")
sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})

setTimeout(() => {
sub.unref()
sub.stdout.unref()
sub.stderr.unref()
}, 1000)

必要な出力が出る程度に 1 秒待ってから unref しています
ちゃんとやるなら stdout の出力から必要なものを受け取れたら実行みたいにしたほうがいいです

まぁ終了させるなら unref していかなくても process.exit でもいいのですけどね

考えてみると常駐してるものから出力を受け取り続ける以上終了しないのが正常ですよね

job.sh で job.js を起動する部分で

./job.js > file 2>&1 &

みたいにしてしまったほうがいいかもしれませんね