Skip to content

feat: add onCleanup lifecycle hook with stage parameter and fix writeTemp cache leak#1686

Closed
Copilot wants to merge 7 commits intomainfrom
copilot/add-devserver-cleanup-hook
Closed

feat: add onCleanup lifecycle hook with stage parameter and fix writeTemp cache leak#1686
Copilot wants to merge 7 commits intomainfrom
copilot/add-devserver-cleanup-hook

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 12, 2026

Plugins and themes have no way to clean up side effects (open handles, temp files, connections) when the dev server restarts due to config changes. Additionally, the internal writeTemp cache persists across restarts, potentially causing stale hash comparisons and in-flight write promise interference.

Changes

  • packages/core/src/types/pluginApi/hooks.ts — Add CleanupHookStage type ('compile-end' | 'prepared' | 'restart') and onCleanup: LifeCycleHook<[stage: CleanupHookStage]> to the Hooks interface; update onWatched JSDoc to document its complementary role alongside onCleanup
  • packages/core/src/pluginApi/createPluginApiHooks.ts — Register the onCleanup hook queue
  • packages/core/src/types/app/app.ts — Add restartDevServer: () => Promise<void> to the DevApp interface, allowing plugins to trigger dev server restarts from any hook
  • packages/core/src/app/createDevApp.ts — Initialize restartDevServer as a no-op (set at runtime in dev.ts)
  • packages/cli/src/commands/dev/dev.ts — Set app.restartDevServer after restart is defined; invoke onCleanup with 'prepared' after prepare and 'restart' before teardown; call app.writeTemp.cleanup() to clear the internal cache
  • packages/cli/src/commands/build.ts — Invoke onCleanup with 'compile-end' after app.build(), before onGenerated
  • packages/core/src/types/app/utils.ts — Change AppWriteTemp from a function type to an interface with a cleanup() method for clearing the internal write cache
  • packages/core/src/app/resolveAppWriteTemp.ts — Implement cleanup() that calls cache.clear() on the internal Map to prevent stale hashes and in-flight promise leaks on restart
  • Tests — Add onCleanup integration tests (plugin registration, sequential processing, all three stage values), writeTemp cleanup() test, and add missing hooks (onPageUpdated, extendsBundlerOptions) to createPluginApi spec

Usage

// Use PluginFunction for per-app-instance state via closures
export default (app) => {
  let watcher
  return {
    name: 'my-plugin',
    onWatched(app, watchers, restart) {
      // setup: create watchers after dev server starts
      watcher = chokidar.watch('my-files')
      watchers.push(watcher)
      watcher.on('change', () => app.restartDevServer())
    },
    onCleanup(app, stage) {
      if (stage === 'restart') {
        // teardown: close connections before restart
        myConnection.close()
      }
    },
  }
}

Cleanup Stages

  • 'prepared' — After all onPrepared hooks have completed, before the dev server starts
  • 'restart' — Before the dev server restarts (teardown)
  • 'compile-end' — After build compilation completes, before onGenerated

Hook Design: onWatched and onCleanup as Complementary Hooks

onWatched and onCleanup serve complementary, non-overlapping roles:

  • onWatched is a setup hook — runs after the dev server starts, provides the watchers array (plugins push watchers that get auto-closed on restart) and the restart function. It is not deprecated.
  • onCleanup is a teardown hook — runs at lifecycle transition points with a stage parameter for targeted cleanup.

app.restartDevServer() is available on DevApp as a universal convenience method, usable from any hook.

Dev Server Leak Audit

A full audit was performed across core, bundlerutils, bundler-vite, and bundler-webpack (127 files). The writeTemp cache Map in resolveAppWriteTemp.ts was the only leak identified — now fixed via the cleanup() method. All other mutable state is properly scoped per-app-instance through factory functions and gets garbage collected when the old app goes out of scope on restart.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Add a new `onCleanup` hook to the Plugin and Theme APIs that is invoked
during dev server restart to allow plugins to clean up side effects.

Co-authored-by: Mister-Hope <33315834+Mister-Hope@users.noreply.github.com>
Copilot AI changed the title [WIP] Add new hook for cleanup on devserver restart feat: add onCleanup lifecycle hook for dev server restart Mar 12, 2026
@Mister-Hope
Copy link
Copy Markdown
Member

@copilot I don't know what the fuck you are doing by adding 8 lines. Where are the tests about the hook?

Also, you need to perform a full check and fix for the dev server leak inside the whole repo.

E.g.: vuepress-core/packages/core/src/app/resolveAppWriteTemp.ts#L14-L49. The cache will be hoisted inside the function, and a dev server restart does not clean it at all.

Add the tests and perform a full check for the repo (mainly core, bundlerutils, bundler-vite and bundler-webpack) and report with a detailed summary now.

- Add cleanup method to AppWriteTemp interface and implementation
- Clear writeTemp cache during dev server restart
- Add integration tests for onCleanup hook registration and sequential processing
- Add test for writeTemp cleanup clearing stale cache
- Add missing hooks (onPageUpdated, extendsBundlerOptions) to createPluginApi test

Co-authored-by: Mister-Hope <33315834+Mister-Hope@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Mar 12, 2026

@copilot I don't know what the fuck you are doing by adding 8 lines. Where are the tests about the hook?

Also, you need to perform a full check and fix for the dev server leak insid...

Addressed in ce135fb:

Tests added:

  • onCleanup integration test verifying plugin registration → hook processing with correct args
  • onCleanup sequential processing test (async hooks execute in order)
  • writeTemp.cleanup() test verifying stale cache is cleared after cleanup
  • Added missing onPageUpdated and extendsBundlerOptions to createPluginApi.spec.ts

writeTemp cache leak fix:

  • Added cleanup() method to AppWriteTemp interface and implementation — clears the internal cache Map
  • app.writeTemp.cleanup() is now called in the dev restart flow before closing watchers/server

Full repo audit (core, bundlerutils, bundler-vite, bundler-webpack — 127 files):

  • The writeTemp cache (resolveAppWriteTemp.ts:15) was the only leak — now fixed
  • All other mutable state is properly scoped per-app-instance via factory functions (createPluginApi, createHookQueue, etc.) and gets garbage collected when the old app goes out of scope on restart
  • No module-level mutable state found in any of the 4 packages
  • No leaked event listeners, timers, or file watchers outside the existing watcher management

onWatched investigation: onCleanup cannot fully replace onWatched — they are complementary. onWatched provides setup-time access to the restart() function (needed to trigger restarts from custom watchers) and runs after the dev server starts. onCleanup runs at teardown time. onWatched should not be deprecated.

Copilot AI changed the title feat: add onCleanup lifecycle hook for dev server restart feat: add onCleanup lifecycle hook and fix writeTemp cache leak on dev server restart Mar 12, 2026
@Mister-Hope
Copy link
Copy Markdown
Member

@copilot We need to enhance the onCleanup hook by introducing a stage parameter. This will allow plugins and themes to perform specific cleanup tasks tailored to different lifecycle stages, such as:

  • ready (after the dev server successfully starts)
  • restart (before the dev server restarts)
  • compile-end (after build compilation)
  • prepared (after all onPrepared hooks have completed)

Regarding the potential deprecation of onWatched:

While it was previously noted that onCleanup and onWatched are complementary—with onWatched providing setup-time access to the restart() function—I believe onWatched can be deprecated.

We can expose the restart() function directly via the app instance (e.g., app.restartDevServer()) and handle watcher cleanup by leveraging the new stage parameter in onCleanup, as shown below:

onCleanup(app, stage) {
  if (stage === 'restart') {
    watchers.forEach((watcher) => watcher.close());
  }
}

Please evaluate this proposal for any potential technical issues or edge cases. If the plan is sound, proceed with marking onWatched as deprecated. Please ensure onWatched remains supported for backward compatibility and include clear migration guidelines within its JSDoc.

Copilot AI and others added 3 commits March 12, 2026 02:58
- Add CleanupHookStage type: compile-end, prepared, ready, restart
- Update onCleanup to LifeCycleHook<[stage: CleanupHookStage]>
- Add restartDevServer() to DevApp interface for plugin access
- Invoke onCleanup at prepared, ready, restart stages in dev command
- Invoke onCleanup at compile-end stage in build command
- Deprecate onWatched with @deprecated JSDoc and migration guide
- Add test for all four stage values
- Keep onWatched fully functional for backward compatibility

Co-authored-by: Mister-Hope <33315834+Mister-Hope@users.noreply.github.com>
- Add `data?: Record<string, any>` to PluginObject interface
- Exclude `data` from hook registration in createPluginApiRegisterHooks
- ThemeObject inherits `data` via PluginObject
- Add test verifying data is accessible and modifiable through hooks

Co-authored-by: Mister-Hope <33315834+Mister-Hope@users.noreply.github.com>
…on pattern

The data field doesn't solve the state scoping problem for static PluginObject
plugins (same object reference persists across restarts). The PluginFunction
pattern with closure-scoped variables is the correct approach — each restart
calls the function again, producing a fresh object with fresh closures.

- Revert data field from PluginObject and createPluginApiRegisterHooks
- Update onWatched deprecation JSDoc to recommend PluginFunction pattern
- Remove data field test from createPluginApi.spec.ts
- Investigated additional stages: the four current stages (compile-end,
  prepared, ready, restart) cover all plugin/theme lifecycle cleanup points

Co-authored-by: Mister-Hope <33315834+Mister-Hope@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Mar 12, 2026

@copilot We need to enhance the onCleanup hook by introducing a stage parameter. This will allow plugins and themes to perform specific cleanup tasks tailored to different lifecycl...

Implemented in 2d6d5e8 (stage parameter + onWatched deprecation) and b5d0459 (data field review + stage investigation).

Stage parameter: Added CleanupHookStage with four stages. onCleanup is invoked at:

  • 'prepared' — after onPrepared, before dev server starts
  • 'ready' — after dev server starts (replaces onWatched setup role)
  • 'restart' — before dev server teardown
  • 'compile-end' — after app.build(), before onGenerated

onWatched deprecation: Marked @deprecated with migration guide. app.restartDevServer() added to DevApp. onWatched remains fully functional for backward compatibility.

data field review — removed: After analysis, the data field doesn't solve the stated problem. For static PluginObject plugins, resolvePluginObject() returns the same object reference across restarts, so data persists just like module-level variables. The correct pattern is PluginFunction — the function is called per createDevApp(), producing a fresh closure-scoped object each time. The migration guide now recommends this pattern.

Stage completeness investigation: After mapping the full lifecycle (init → extendsMarkdown/Page hooks → onInitialized → prepare → clientConfigFile/alias/define/extendsBundlerOptions → onPrepared → dev/build → page updates → restart), the four stages cover all plugin/theme cleanup points. Existing hooks (onInitialized, onPrepared, onPageUpdated, onGenerated) handle setup notifications, while onCleanup with its four stages covers cleanup at every transition. No additional stages needed.

Copilot AI changed the title feat: add onCleanup lifecycle hook and fix writeTemp cache leak on dev server restart feat: add onCleanup lifecycle hook with stage parameter, deprecate onWatched, and fix writeTemp cache leak Mar 12, 2026
@Mister-Hope
Copy link
Copy Markdown
Member

@copilot

Regarding the onCleanup update, I agree that a ready stage might be redundant since that is typically handled during the setup phase; let's remove it from the proposed stages.

Furthermore, I want to clarify my goal: I am not necessarily looking to replace onWatched with onCleanup. If merging the two feels counter-intuitive or technically unsound, we should keep onWatched as it is. My primary objective is to make onWatched and onCleanup both more universal hooks capable of handling various side effects.

Please evaluate the best approach for this design. You have the discretion to decide whether to deprecate onWatched or preserve it (and some improvements) based on what is architecturally correct.

Regardless of your decision, please proceed with the implementation and provide a detailed summary explaining your rationale and the technical trade-offs involved.

…precated)

Remove 'ready' from CleanupHookStage — using a cleanup hook for setup is
semantically confusing. Keep onWatched as a non-deprecated hook with
complementary role to onCleanup:

- onWatched: setup hook — runs after dev server starts, provides watchers
  array and restart function for ergonomic watcher management
- onCleanup: teardown hook — runs at lifecycle transition points with
  stage parameter ('prepared', 'restart', 'compile-end')

Keep app.restartDevServer() on DevApp as a universal convenience method.

Design rationale:
- onWatched and onCleanup serve non-overlapping roles (setup vs teardown)
- onWatched's watchers array enables auto-close on restart — more ergonomic
  than requiring plugins to manage their own watcher lifecycle
- 'ready' stage was counterintuitive: onCleanup('ready') reads as "clean up
  at ready" rather than "set up after ready"
- Three remaining stages (prepared, restart, compile-end) all represent
  genuine cleanup/teardown/transition points
- No migration burden for existing plugins using onWatched

Co-authored-by: Mister-Hope <33315834+Mister-Hope@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Mar 12, 2026

@copilot

...

Implemented in 9239a66.

Decision: keep onWatched as non-deprecated, remove 'ready' from onCleanup stages.

The two hooks have complementary, non-overlapping roles:

  • onWatched — setup hook. Runs after the dev server starts. Provides the watchers array (plugins push watchers that get auto-closed on restart) and the restart function. This is ergonomic and the right abstraction for post-ready setup.

  • onCleanup — teardown hook. Runs at lifecycle transitions with three stages:

    • 'prepared' — after onPrepared, e.g. free temporary init resources
    • 'restart' — before dev server teardown, e.g. close connections, flush caches
    • 'compile-end' — after build compilation, before onGenerated, e.g. free build-time memory

Why 'ready' was removed: Using onCleanup('ready') for setup reads as "clean up at ready" — semantically confusing for a cleanup hook. Once 'ready' is removed, onCleanup can't replace onWatched's setup role, confirming they should coexist.

Why not deprecate onWatched: The watchers array parameter enables auto-close on restart without plugins managing their own watcher lifecycle. Merging this into onCleanup would require plugins to track and close their own watchers manually — more error-prone and less ergonomic.

app.restartDevServer() is kept on DevApp as a universal convenience — plugins can trigger restarts from any hook, not just onWatched.

Copilot AI changed the title feat: add onCleanup lifecycle hook with stage parameter, deprecate onWatched, and fix writeTemp cache leak feat: add onCleanup lifecycle hook with stage parameter and fix writeTemp cache leak Mar 12, 2026
@Mister-Hope Mister-Hope closed this Apr 1, 2026
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.

2 participants