Repository navigation
fix: decoContent() Vite plugin reloads open pages after the CMS has the saved content - #690
Merged
Merged
Conversation
…the saved content Vite sites carried two pieces of template code to show a block saved with `deco serve`: an import.meta.hot.accept block in src/cms.ts that called createCMS again and sent an event, and a vite.config.ts plugin that turned the event into full-reload. @decocms/blocks/vite now ships it as one opt-in plugin, decoContent() (apply: "serve"). In the server environments it appends a self-accepting HMR footer to .deco/blocks.gen.ts: the new module goes to the CMS already created for that .deco folder (adoptContent, from @decocms/blocks/vite/hot, keeping that CMS's own options), then the module sends decocms:content-updated; configureServer turns that into one full-reload of the client (debounced, as a save swaps the module twice: JSON, then blocks.gen.ts). src/cms.ts never runs again, so "client is not a function" stays fixed. The generated file on disk and Next.js sites are unchanged; vite is an optional peer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…res next to decoContent() Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
… server environments
The footer's import of @decocms/blocks/vite/hot only appears after the
transform, so the dependency scan never saw it. With @decocms/blocks
installed from npm and @cloudflare/vite-plugin, the worker discovered it on
the first request, re-optimized its dependencies and reloaded the whole
program ("new dependencies optimized: @decocms/blocks/vite/hot").
configEnvironment now adds it to optimizeDeps.include for every non-client
environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ts updates The content module accepting its own updates still left every module above it stale: Vite invalidates the importers of an updated module unless they accept it, and TanStack Start imports its server entry again on each request, so src/cms.ts and the route tree re-ran on every save and bursts of saves gave 500s (handleServerAction reading `method` of undefined, Route undefined in routeTree.gen). In the server environments the plugin now appends, to each module that statically imports the site's .deco/blocks.gen (any specifier that resolves to it: relative, alias, with or without the extension), the accept block sites used to write by hand: import.meta.hot.accept(<specifier>, next => adoptContent(next.default), then the content-updated event). The importer is the update's boundary, so nothing above it is invalidated. A module that already accepts the content module by hand is left as it is. The content module itself is no longer transformed. optimizeDeps include and the 100 ms reload debounce are unchanged. The dev-server test now imports its entry again on every request, as TanStack Start does, and asserts that neither cms.ts nor the entry re-runs over three saves (the self-accepting design runs them 4 times); a second test covers a module with its own accept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
|
🎉 This PR is included in version 8.2.2 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vite sites no longer need template code to show a block saved with
deco serve. Today every v8 Vite site carries two pieces: animport.meta.hot.accept("../.deco/blocks.gen", ...)block insrc/cms.tsthat callscreateCMSagain and sendssite:content-updated, and asite-content-reloadplugin invite.config.tsthat turns that event intofull-reload. This PR ships that as one opt-in Vite plugin:Only Vite sites add it. Next.js sites never import it, so nothing changes for them. The generated
.deco/blocks.gen.tson disk is unchanged (deco contentis untouched).Supersedes #676. That PR fixed only the timing, in the docs (reload after
createCMStakes the new module, not on the file change). This PR makes the plugin do that and rewrites the same "Editing locally" section, so #676 can be closed.How it works
@decocms/blocks/vite,decoContent():apply: "serve". In a server environment only (this.environment.config.consumer === "server", orssrbefore the Environment API), itstransformhook finds each module that statically imports the site's.deco/blocks.gen.ts. It reads the module's static imports and re-exports and resolves each one withthis.resolve, so any specifier form counts:"../.deco/blocks.gen", an alias like"~/.deco/blocks.gen", with or without.ts. Modules undernode_modules, non-JS files and the content module itself are skipped. To each importer (usuallysrc/cms.ts) it appends the block sites used to write by hand:map: null). The importer is the update's HMR boundary, so neither it nor anything above it (the router entry,routeTree.gen) is invalidated or runs again. JSON block edits bubble up throughblocks.gen.tsto the same boundary.….accept(<specifier>)(string or array) that resolves to the content module, the plugin adds nothing to it.configEnvironmentadds@decocms/blocks/vite/hottooptimizeDeps.includein the server environments. The appended import appears only after the transform, so the dependency scan can't see it. Discovered on the first request instead, the worker re-optimizes its dependencies and reloads the whole program once.configureServerlistens for the event on every server environment (the@cloudflare/vite-pluginworker whatever it's named,rsc, plainssr) and sends{ type: "full-reload" }to the client environment. It waits 100 ms because a save swaps the module twice (the block's JSON, thenblocks.gen.ts), so open pages reload once.adoptContent(content)(src/v8/cms.ts, exposed only on@decocms/blocks/vite/hot): hands the module to every CMS instance keyed for that content's.decoroot (module:<root>, with or without|site:…), the same adopt path a secondcreateCMScall takes. It doesn't need the site's config: each instance re-adopts with its own stored config, so a hosted CMS keeps itssite/dev. It returns how many instances took it. With none (no CMS yet, or a module withoutroot) it does nothing.viteis an optional peer (>=6.0.0, for the Environment API) and a devDependency. The plugin usesimport typeonly, so nothing is imported from vite at runtime. Newexportsentries are./viteand./vite/hot(types/source/default, like the others), andbun run buildemitsdist/v8/vite/*.Sites that still have the old code: they keep working.
cms.tsaccept block both in place, the plugin leaves that module alone. The old callback re-creates the CMS and the oldsite-content-reloadplugin reloads the page, as before. If you delete only the old Vite plugin, pages don't reload, so the migration reference says to delete both together.hotUpdatehook on the file change. That hook fires early, so the reference says to delete it.Review fixes
optimizeDeps(041a9b4): the server environments pre-bundle@decocms/blocks/vite/hot. Without that, the first request found the new import and re-optimized the worker, which reloaded the whole program once.blocks.gen.tsaccept its own updates. Vite still invalidates every importer of an updated module unless the importer accepts it. TanStack Start imports its server entry again on each request, so with the cloudflare pluginsrc/cms.tsand the route tree re-ran on every save. On the demo store, bursts of saves gave 500s (Cannot read properties of undefined (reading 'method')inhandleServerAction,Routeundefined inrouteTree.gen), and the client then stopped reloading. The plugin now writes the accept that worked before (the hand-writtencms.tsblock had 14/14 clean bursts), on the importer.Tests
src/v8/vite/vite.test.ts(unit tests):adoptContent: the same instance, another.decoroot left alone, a hosted instance keeps its options, nothing before a CMS exists or for a root-less module.import()orimport.meta. It also finds accepted dependencies in string and array forms..tsand bare-alias specifiers resolving to the content module get the block with their own specifier. Nothing is added in the client, to the content module itself, undernode_modules, to non-JS files, to modules that don't import it, or to a module that accepts it by hand. An unrelated accept doesn't count. Thessrflag works without the Environment API.optimizeDeps.includeonly in server environments;configureServerlistens on every server environment, and two events make onefull-reload.src/v8/vite/devServer.test.ts(integration): a real Vite dev server (createServer, SSRRunnableDevEnvironment). Like TanStack Start, every "request" imports the site entry again (entry.ts → cms.ts, which imports~/.deco/blocks.genthrough an alias).full-reload, and a request at that moment already resolves the new content.cms.tsand the entry each ran once, and the transformedcms.tshas exactly one accept whileblocks.gen.tshas none. With the previous self-accepting design the same test fails with{ cms: 4, entry: 4 }.cms.tswith its own accept block keeps exactly one accept, its callback runs, and the plugin sends no reload.@cloudflare/vite-plugin(Nodevite dev; 10 bursts of 3 saves 120 ms apart, each save writing the block JSON and thenblocks.gen.ts; two request loops running throughout; avite-hmrWebSocket counting reloads;cms.tslogging each run):cms.tsran once in all,hot updated: /.deco/blocks.gen.ts via /src/cms.ts, and the page had each burst's last save 10/10.cms.tsran 16 times, and 1 burst's page lacked the last save.examples/tanstack-smoke: 276/276 responses 200,cms.tsran once, and 10/10 bursts showed the last save. Saves more than 100 ms apart each get their own reload, so a burst gave 1 to 3 reloads, never more than its saves.bun run check(typecheck, biome, knip),bun run buildandskills:checkpass. Inbun run test, 1926 tests pass. The failures are not from this PR:delivery.cli.test.tsDS-2: it grepsprotocol/storage/fs/git.tsfor"reset", which fix(protocol): discard also unstages the discarded blocks #689 added. It fails on the base too.schema.test.ts(is deterministic, and one more): they hit the 5 s timeout under full-suite load and pass when run alone (58/58).Also in this PR
examples/tanstack-smokeandexamples/tanstack-rsc-smokeadddecoContent(). Thets-09guide conformance test now expects that one@decocms/blocks/viteimport..agents/skills/deco-v7-to-v8-migration/reference/content.md, "Editing locally": add the plugin; drop the old snippet and plugin together when you do; and why: the "client is not a function" and one-edit-behind history from docs(skills): reload open pages after the server has the saved content #676, and why the boundary is the importer.README.mdmentions the plugin and the/vitesubpath.Follow-ups opened alongside:
🤖 Generated with Claude Code
https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig