Skip to content

Remove nodemon from dev server lifecycle - #4407

Open
infomiho wants to merge 7 commits into
miho/job-output-plumbingfrom
miho/native-server-process-watch
Open

Remove nodemon from dev server lifecycle#4407
infomiho wants to merge 7 commits into
miho/job-output-plumbingfrom
miho/native-server-process-watch

Conversation

@infomiho

@infomiho infomiho commented Jul 1, 2026

Copy link
Copy Markdown
Member

Description

Remove nodemon from the generated development server. wasp start now owns the server process, decides when it needs updating, and cleans it up itself.

This fixes stale server processes after a failed compile or bundle. It also avoids restarting a healthy server when a recompile did not change anything the server uses.

Closes #1844.

Server effects

Every successful recompile produces one ServerEffect:

Effect Meaning
NoServerEffect Keep the current server.
RestartServer Restart the existing bundle, for example after .env changes.
RebundleAndRestartServer Build a new server bundle, then replace the server.

When several files change together, the strongest required effect wins.

The effect is based on both the saved project files and the generated files whose checksums actually changed. A main.wasp.ts save alone does not restart the server. If that edit changes generated server/src, server/package.json, or server/.env, it does.

Trade-off: generated SDK paths are ignored. We assume every server-relevant SDK change also changes generated server source or a project src runtime input.

Server process controller

The controller owns the current server process and handles compile results one at a time:

Result Action
Compile fails Stop the server and keep wasp start running.
Server inputs are unchanged Keep the current server.
Only runtime configuration changes Restart without bundling.
Server inputs change and bundle succeeds Replace the server.
Server inputs change and bundle fails Stop the server and keep wasp start running.

If the server exits by itself, the controller cleans up any surviving descendants and waits for the next successful compile.

Managed subprocesses

Wasp.Job.Subprocess.spawn starts a child process whose lifetime is tied to its Job. Releasing it stops the whole owned process tree, drains or closes output, and waits for cleanup before returning.

The platform-specific process-tree work lives in Wasp.Job.Subprocess.System: process groups and signals on Unix, process jobs on Windows, and Linux /proc checks for zombie-only groups.

wasp start also installs graceful SIGINT/SIGTERM handling so process cleanup finishes before Wasp exits.

Review map

Just code moving:

  • Shared process-test helpers move into Test.Process.Util; their behavior is unchanged.

Generated or mechanical changes:

  • Example lockfiles only reflect removing the generated nodemon dependency.
  • E2E snapshots were regenerated after deleting nodemon.json and changing the generated server package scripts.
  • CLI command signature edits mostly thread the compile hooks and server controller through existing flows.

Not just a rename:

  • The old long-running process code is split into managed lifecycle code and platform-specific system code. The public API also changes so cleanup is owned by Job resources.

Type of change

  • 🔧 Just code/docs improvement
  • 🐞 Bug fix
  • 🚀 New/improved feature
  • 💥 Breaking change

Checklist

  • I tested my change in a Wasp app to verify that it works as intended.

  • 🧪 Tests and apps:

    • I added unit tests for my change.
    • (if you fixed a bug) I added a regression test for the bug I fixed.
    • (if you added/updated a feature) I added/updated e2e tests in examples/kitchen-sink/e2e-tests.
    • (if you added/updated a feature) I updated the starter templates in waspc/data/Cli/templates, as needed.
    • (if you added/updated a feature) I updated the example apps in examples/, as needed.
      • (if you updated examples/tutorials) I updated the tutorial in the docs (and vice versa).
  • 📜 Documentation:

    • (if you added/updated a feature) I added/updated the documentation in web/docs/.
  • 🆕 Changelog: (if change is more than just code/docs improvement)

    • I updated waspc/ChangeLog.md with a user-friendly description of the change.
    • (if you did a breaking change) I added a step to the current migration guide in web/docs/migration-guides/.
    • I bumped the version in waspc/waspc.cabal to reflect the changes I introduced.

@infomiho
infomiho temporarily deployed to fly-deploy-test July 1, 2026 08:11 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 1, 2026 08:11 — with GitHub Actions Inactive
@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@wasp.sh/spec

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/spec@4407

@wasp.sh/wasp-cli

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli@4407

@wasp.sh/wasp-cli-darwin-arm64-unknown

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-darwin-arm64-unknown@4407

@wasp.sh/wasp-cli-darwin-x64-unknown

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-darwin-x64-unknown@4407

@wasp.sh/wasp-cli-linux-arm64-glibc

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-arm64-glibc@4407

@wasp.sh/wasp-cli-linux-x64-glibc

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-x64-glibc@4407

@wasp.sh/wasp-cli-linux-x64-musl

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-x64-musl@4407

commit: ef380f5

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying wasp-docs-on-main with  Cloudflare Pages  Cloudflare Pages

Latest commit: ef380f5
Status: ✅  Deploy successful!
Preview URL: https://a5648703.wasp-docs-on-main.pages.dev
Branch Preview URL: https://miho-native-server-process-w.wasp-docs-on-main.pages.dev

View logs

@infomiho
infomiho temporarily deployed to fly-deploy-test July 1, 2026 11:41 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 1, 2026 11:41 — with GitHub Actions Inactive
import qualified Wasp.Job as J
import Wasp.Job.Except (ExceptJob, toExceptJob)
import Wasp.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv)
import Wasp.Job.Node (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were some Node.js specific bits that got mixed with generic process handling, so I separated them.

@infomiho
infomiho had a problem deploying to railway-deploy-test July 1, 2026 16:11 — with GitHub Actions Failure
@infomiho
infomiho temporarily deployed to fly-deploy-test July 1, 2026 16:11 — with GitHub Actions Inactive
@infomiho
infomiho had a problem deploying to railway-deploy-test July 1, 2026 17:02 — with GitHub Actions Error
@infomiho
infomiho temporarily deployed to fly-deploy-test July 1, 2026 17:28 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 1, 2026 17:28 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 2, 2026 13:24 — with GitHub Actions Inactive
@infomiho
infomiho had a problem deploying to railway-deploy-test July 2, 2026 13:24 — with GitHub Actions Failure
Comment on lines +11 to +34
#if !mingw32_HOST_OS
import qualified System.Posix.Signals as Signals
#endif

withGracefulTermination :: IO a -> IO a
#if mingw32_HOST_OS
withGracefulTermination = id
#else
withGracefulTermination action = do
targetThreadId <- myThreadId
let interruptCurrentCommand = Signals.Catch $ throwTo targetThreadId UserInterrupt
bracket
(mapM (installHandler interruptCurrentCommand) [Signals.sigINT, Signals.sigTERM])
restoreHandlers
(const action)
where
installHandler handler signal = do
previousHandler <- Signals.installHandler signal handler Nothing
return (signal, previousHandler)

restoreHandlers =
mapM_ $ \(signal, previousHandler) ->
Signals.installHandler signal previousHandler Nothing
#endif

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the wildest bit of the implementation - to enable cleanup after wasp start is interrupted.

@cprecioso cprecioso Jul 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is bracket not enough?
Also, I am adding Resourcet to Command in #4504, might that be useful?

@@ -0,0 +1,29 @@
module Wasp.Util.Glob

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract from waspignore module. Shared between waspignore and server path watching.

-- Also makes sure to remove any redundant file drafts that have been left on the disk from before.
-- It is smart when writing, so it doesn't write file drafts that are already written on the disk from before.
synchronizeFileDraftsWithDisk :: Path' Abs (Dir GeneratedAppDir) -> [FileDraft] -> IO ()
synchronizeFileDraftsWithDisk :: Path' Abs (Dir GeneratedAppDir) -> [FileDraft] -> IO [GeneratedAppPathChange]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After sync file drafts, we return a list of written and deleted files. The idea is to be able to detect which generated files changes happened so we can react to it e.g. restart the server process.

@infomiho
infomiho temporarily deployed to railway-deploy-test July 2, 2026 15:55 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 2, 2026 15:55 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 2, 2026 18:01 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 2, 2026 18:37 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 2, 2026 18:37 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 3, 2026 10:21 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 3, 2026 10:21 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 3, 2026 13:23 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 3, 2026 13:23 — with GitHub Actions Inactive
@infomiho
infomiho had a problem deploying to railway-deploy-test July 3, 2026 14:22 — with GitHub Actions Error
@infomiho
infomiho force-pushed the miho/native-server-process-watch branch 2 times, most recently from 8a731a0 to 9e7d550 Compare July 17, 2026 15:40
@infomiho
infomiho temporarily deployed to railway-deploy-test July 17, 2026 16:15 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 17, 2026 16:15 — with GitHub Actions Inactive
@infomiho
infomiho changed the base branch from miho/glob-matching to miho/job-output-plumbing July 20, 2026 13:15
@infomiho
infomiho temporarily deployed to railway-deploy-test July 20, 2026 13:35 — with GitHub Actions Inactive
@infomiho
infomiho force-pushed the miho/native-server-process-watch branch from 82296d7 to 5bd61ac Compare July 20, 2026 13:53
@infomiho
infomiho temporarily deployed to fly-deploy-test July 20, 2026 14:06 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 20, 2026 14:06 — with GitHub Actions Inactive
@infomiho
infomiho force-pushed the miho/native-server-process-watch branch from 5bd61ac to 063ac16 Compare July 20, 2026 14:49
@infomiho
infomiho temporarily deployed to railway-deploy-test July 20, 2026 15:33 — with GitHub Actions Inactive
@infomiho
infomiho force-pushed the miho/native-server-process-watch branch from 063ac16 to 12e2fca Compare July 21, 2026 08:56
@infomiho
infomiho temporarily deployed to fly-deploy-test July 21, 2026 09:13 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 21, 2026 09:13 — with GitHub Actions Inactive
@infomiho
infomiho marked this pull request as ready for review July 21, 2026 09:13
@infomiho
infomiho requested a review from a team as a code owner July 21, 2026 09:13
@infomiho
infomiho requested review from cprecioso and removed request for a team July 21, 2026 09:13

@cprecioso cprecioso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random comments, didn't do proper review yet

Comment thread waspc/ChangeLog.md
### 🐞 Bug fixes

- Fixed a race condition where `useAuth()` could return stale user data after an action that modifies the `User` entity (by @okxint). ([#4343](https://github.com/wasp-lang/wasp/issues/4343))
- `wasp start` now manages the development server process directly instead of relying on the generated server's `nodemon`, improving restart behavior. ([#4407](https://github.com/wasp-lang/wasp/pull/4407))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick

Suggested change
- `wasp start` now manages the development server process directly instead of relying on the generated server's `nodemon`, improving restart behavior. ([#4407](https://github.com/wasp-lang/wasp/pull/4407))
- `wasp start` now manages the development server process directly instead with `nodemon`, improving stability. ([#4407](https://github.com/wasp-lang/wasp/pull/4407))

@@ -1 +1 @@
`npm start` to run server in development mode (it reloads on changes).
`npm start` runs the already bundled server in development mode. Use `wasp start` for automatic rebuilds and restarts while developing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove the wasp start note straight away. Users either see this in production or don't see it in development.
I might even remove the README.

devServerStartExecutable = "node"

devServerStartArgs :: [String]
devServerStartArgs = ["--enable-source-maps", "-r", "dotenv/config", "bundle/server.js"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +150 to +151
devServerStartExecutable :: String
devServerStartExecutable = "node"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick i'd make the command + args just a single array, we can head it later. or a tuple at least. it doesn't make sense to define one thing without the other IMO

Comment on lines +156 to +157
devServerStartCommand :: String
devServerStartCommand = unwords $ devServerStartExecutable : devServerStartArgs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and we could just unwords this in the callsite, we "don't know" here yet where this will be used and each callsite is in charge of formatting multiple arguments and escaping spaces etc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not reviewing this yet, maybe the review on #4500 makes it easier

Comment on lines +179 to +183
#if mingw32_HOST_OS
stopProcessTree processHandle rootExitAsync _ = do
void (try (P.terminateProcess processHandle) :: IO (Either SomeException ()))
waitForAsync rootExitAsync hardStopTimeoutMicroseconds
#else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd extract the system-specific parts into their own module (e.g. Job.Process.LongRunning.System), so the templated functions are more scoped to a single place instead of mixed with "regular" code

Comment on lines +13 to +21
recursiveFileGlobsWithExtensions :: FilePath -> [String] -> [String]
recursiveFileGlobsWithExtensions dir extensions =
(\ext -> dir FP.</> "**" FP.</> "*" ++ ext) <$> extensions

dirAndDescendantsGlobs :: FilePath -> [String]
dirAndDescendantsGlobs dir =
[ dir,
dir FP.</> "**" FP.</> "*"
]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patterns are Monoids, so we can use its type directly and <> instead of FilePaths, which is not semantically accurate. Plus the semantic helpers at System.FilePath.Glob.Primitive. We can use / directly since it's the only valid separator for globs, independently of the system:

Suggested change
recursiveFileGlobsWithExtensions :: FilePath -> [String] -> [String]
recursiveFileGlobsWithExtensions dir extensions =
(\ext -> dir FP.</> "**" FP.</> "*" ++ ext) <$> extensions
dirAndDescendantsGlobs :: FilePath -> [String]
dirAndDescendantsGlobs dir =
[ dir,
dir FP.</> "**" FP.</> "*"
]
recursiveFileGlobsWithExtensions :: Pattern -> [String] -> [Patterns]
recursiveFileGlobsWithExtensions dir extensions =
((dir <> "/**/*" <>) . literal) <$> extensions
dirAndDescendantsGlobs :: Pattern -> [Pattern]
dirAndDescendantsGlobs dir =
[ dir,
dir <> "/**/*"
]

(i tried it in GHCi and it all works as expected)

Comment thread waspc/src/Wasp/Project.hs
Path' Abs (Dir Generator.GeneratedAppDir) ->
CompileOptions ->
IO ([CompileWarning], [CompileError])
IO CompileResult

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like what we actually want is a

type Compilation = ExceptT CompileError (WriterT [CompileWarning] IO) [FileOrDirPathRelativeTo Generator.GeneratedAppDir]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate how our file watching works

2 participants