From 82046984c83ecd273fec99cd1be37756d5d59f7a Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Fri, 10 Apr 2026 10:05:06 -0700 Subject: [PATCH 1/2] fix: marko virtual file hmr --- .changeset/happy-rooms-burn.md | 5 ++ .../__snapshots__/build.expected.md | 8 ++ .../__snapshots__/dev-hmr.expected.md | 17 ++++ .../__snapshots__/dev.expected.md | 8 ++ .../dev-server.mjs | 43 ++++++++++ .../server.mjs | 16 ++++ .../src/index.js | 9 ++ .../src/tags/child-tag.marko | 5 ++ .../src/tags/layout.marko | 9 ++ .../src/template.marko | 9 ++ .../test.config.ts | 8 ++ .../__snapshots__/build.expected.md | 14 ++++ .../__snapshots__/dev-hmr.expected.md | 42 ++++++++++ .../__snapshots__/dev.expected.md | 14 ++++ .../dev-server.mjs | 43 ++++++++++ .../isomorphic-virtual-file-hmr/server.mjs | 16 ++++ .../src/components/class-component.marko | 18 ++++ .../src/components/implicit-component.marko | 9 ++ .../src/components/layout-component.marko | 15 ++++ .../isomorphic-virtual-file-hmr/src/index.js | 9 ++ .../src/template.marko | 9 ++ .../test.config.ts | 20 +++++ src/__tests__/main.test.ts | 38 +++++---- .../utils/inject-hmr-events-plugin.ts | 46 +++++++++++ src/index.ts | 82 ++++++++++++------- 25 files changed, 466 insertions(+), 46 deletions(-) create mode 100644 .changeset/happy-rooms-burn.md create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/build.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/dev-server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/index.js create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/child-tag.marko create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/layout.marko create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/template.marko create mode 100644 src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/test.config.ts create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/build.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/dev-server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/class-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/implicit-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/layout-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/index.js create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/template.marko create mode 100644 src/__tests__/fixtures/isomorphic-virtual-file-hmr/test.config.ts create mode 100644 src/__tests__/utils/inject-hmr-events-plugin.ts diff --git a/.changeset/happy-rooms-burn.md b/.changeset/happy-rooms-burn.md new file mode 100644 index 0000000..7ef9b30 --- /dev/null +++ b/.changeset/happy-rooms-burn.md @@ -0,0 +1,5 @@ +--- +"@marko/vite": patch +--- + +Fix marko virtual file HMR diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/build.expected.md b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/build.expected.md new file mode 100644 index 0000000..dc42c20 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/build.expected.md @@ -0,0 +1,8 @@ +# Loading + +```html +
+ Color: rgb(0, 128, 0) +
+``` + diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md new file mode 100644 index 0000000..8e69280 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md @@ -0,0 +1,17 @@ +# Loading + +```html +
+ Color: rgb(0, 128, 0) +
+``` + +# HMR 0 +src/template.marko: "div { color: green }" → "div { color: blue }" + +```diff +- Color: rgb(0, 128, 0) ++ Color: rgb(0, 0, 255) + +``` + diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev.expected.md b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev.expected.md new file mode 100644 index 0000000..dc42c20 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev.expected.md @@ -0,0 +1,8 @@ +# Loading + +```html +
+ Color: rgb(0, 128, 0) +
+``` + diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/dev-server.mjs b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/dev-server.mjs new file mode 100644 index 0000000..293d792 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/dev-server.mjs @@ -0,0 +1,43 @@ +// In dev we'll start a Vite dev server in middleware mode, +// and forward requests to our http request handler. + +import { createRequire } from "module"; +import path from "path"; +import url from "url"; +import { createServer } from "vite"; + +// change to import once marko-vite is updated to ESM +const markoPlugin = createRequire(import.meta.url)("../../..").default; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const devServer = await createServer({ + root: __dirname, + appType: "custom", + logLevel: "warn", + plugins: [markoPlugin()], + optimizeDeps: { force: true }, + server: { + ws: false, + hmr: false, + middlewareMode: true, + watch: { + ignored: ["**/node_modules/**", "**/dist/**", "**/__snapshots__/**"], + }, + }, + build: { + assetsInlineLimit: 0, + }, +}); + +export default devServer.middlewares.use(async (req, res, next) => { + try { + const { handler } = await devServer.ssrLoadModule( + path.join(__dirname, "./src/index.js"), + ); + await handler(req, res, next); + } catch (err) { + devServer.ssrFixStacktrace(err); + return next(err); + } +}); diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/server.mjs b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/server.mjs new file mode 100644 index 0000000..76060b9 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/server.mjs @@ -0,0 +1,16 @@ +// In production, simply start up the http server. +import { createServer } from "http"; +import path from "path"; +import serve from "serve-handler"; +import url from "url"; + +import { handler } from "./dist/index.mjs"; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const serveOpts = { public: path.resolve(__dirname, "dist") }; + +export default createServer(async (req, res) => { + await handler(req, res); + if (res.headersSent) return; + await serve(req, res, serveOpts); +}); diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/index.js b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/index.js new file mode 100644 index 0000000..e36d061 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/index.js @@ -0,0 +1,9 @@ +import template from "./template.marko"; + +export function handler(req, res) { + if (req.url === "/") { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + template.render({}).pipe(res); + } +} diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/child-tag.marko b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/child-tag.marko new file mode 100644 index 0000000..065c97b --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/child-tag.marko @@ -0,0 +1,5 @@ + + +
Color: ${color}
diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/layout.marko b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/layout.marko new file mode 100644 index 0000000..08de902 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/tags/layout.marko @@ -0,0 +1,9 @@ + + + + Hello World + + + <${input.content}/> + + diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/template.marko b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/template.marko new file mode 100644 index 0000000..4888a50 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/src/template.marko @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/test.config.ts b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/test.config.ts new file mode 100644 index 0000000..997271b --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/test.config.ts @@ -0,0 +1,8 @@ +export const ssr = true; +export const hmr = [ + { + changes: [ + ["src/template.marko", "div { color: green }", "div { color: blue }"], + ], + }, +]; diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/build.expected.md b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/build.expected.md new file mode 100644 index 0000000..f97ca9c --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/build.expected.md @@ -0,0 +1,14 @@ +# Loading + +```html +
+
+ Color: rgb(0, 128, 0) +
+
+``` + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md new file mode 100644 index 0000000..67055be --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md @@ -0,0 +1,42 @@ +# Loading + +```html +
+
+ Color: rgb(0, 128, 0) +
+
+``` + +# HMR 0 +src/template.marko: "div { color: green }" → "div { color: blue }" + +(no change) + +# HMR 0 Step 0 +await page.click("#clickable") + +```diff +- Color: rgb(0, 128, 0) ++ Color: rgb(0, 0, 255) + +``` + +# HMR 1 +src/template.marko: "div { color: blue }" → "div { color: red }" + +(no change) + +# HMR 1 Step 0 +await page.click("#clickable") + +```diff +- Color: rgb(0, 0, 255) ++ Color: rgb(255, 0, 0) + +``` + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev.expected.md b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev.expected.md new file mode 100644 index 0000000..f97ca9c --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev.expected.md @@ -0,0 +1,14 @@ +# Loading + +```html +
+
+ Color: rgb(0, 128, 0) +
+
+``` + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/dev-server.mjs b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/dev-server.mjs new file mode 100644 index 0000000..293d792 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/dev-server.mjs @@ -0,0 +1,43 @@ +// In dev we'll start a Vite dev server in middleware mode, +// and forward requests to our http request handler. + +import { createRequire } from "module"; +import path from "path"; +import url from "url"; +import { createServer } from "vite"; + +// change to import once marko-vite is updated to ESM +const markoPlugin = createRequire(import.meta.url)("../../..").default; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const devServer = await createServer({ + root: __dirname, + appType: "custom", + logLevel: "warn", + plugins: [markoPlugin()], + optimizeDeps: { force: true }, + server: { + ws: false, + hmr: false, + middlewareMode: true, + watch: { + ignored: ["**/node_modules/**", "**/dist/**", "**/__snapshots__/**"], + }, + }, + build: { + assetsInlineLimit: 0, + }, +}); + +export default devServer.middlewares.use(async (req, res, next) => { + try { + const { handler } = await devServer.ssrLoadModule( + path.join(__dirname, "./src/index.js"), + ); + await handler(req, res, next); + } catch (err) { + devServer.ssrFixStacktrace(err); + return next(err); + } +}); diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/server.mjs b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/server.mjs new file mode 100644 index 0000000..76060b9 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/server.mjs @@ -0,0 +1,16 @@ +// In production, simply start up the http server. +import { createServer } from "http"; +import path from "path"; +import serve from "serve-handler"; +import url from "url"; + +import { handler } from "./dist/index.mjs"; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const serveOpts = { public: path.resolve(__dirname, "dist") }; + +export default createServer(async (req, res) => { + await handler(req, res); + if (res.headersSent) return; + await serve(req, res, serveOpts); +}); diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/class-component.marko b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/class-component.marko new file mode 100644 index 0000000..2feaf63 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/class-component.marko @@ -0,0 +1,18 @@ +class { + onCreate() { + this.state = { + color: undefined + }; + } + onMount() { + this.updateColor(); + } + + updateColor() { + this.state.color = document.querySelector("#clickable").computedStyleMap().get("color"); + } +} + + + Color: ${state.color} + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/implicit-component.marko b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/implicit-component.marko new file mode 100644 index 0000000..0225f24 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/implicit-component.marko @@ -0,0 +1,9 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Implicit Component"); + } +} + + + + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/layout-component.marko b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/layout-component.marko new file mode 100644 index 0000000..9a1c58e --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/components/layout-component.marko @@ -0,0 +1,15 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Layout Component"); + } +} + + + + + Hello World + + + <${input.renderBody}/> + + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/index.js b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/index.js new file mode 100644 index 0000000..d3f5422 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/index.js @@ -0,0 +1,9 @@ +import template from "./template.marko"; + +export function handler(req, res) { + if (req.url === "/") { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + template.render({}, res); + } +} diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/template.marko b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/template.marko new file mode 100644 index 0000000..7080531 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/src/template.marko @@ -0,0 +1,9 @@ +style { + div { color: green } +} + + + + + + diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/test.config.ts b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/test.config.ts new file mode 100644 index 0000000..0be7322 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/test.config.ts @@ -0,0 +1,20 @@ +export const ssr = true; +export const hmr = [ + { + changes: [ + ["src/template.marko", "div { color: green }", "div { color: blue }"], + ], + async steps() { + await page.click("#clickable"); + }, + }, + + { + changes: [ + ["src/template.marko", "div { color: blue }", "div { color: red }"], + ], + async steps() { + await page.click("#clickable"); + }, + }, +]; diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index f7e581a..6576f54 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -12,6 +12,7 @@ import * as playwright from "playwright"; import url from "url"; import markoPlugin, { type Options } from ".."; +import injectHmrEventsPlugin from "./utils/inject-hmr-events-plugin"; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); @@ -238,13 +239,15 @@ for (const fixture of fs.readdirSync(FIXTURES)) { root: dir, server: { port, - watch: { - ignored: [ - "**/node_modules/**", - "**/dist/**", - "**/__snapshots__/**", - ], - }, + hmr: false, + watch: null, + // watch: { + // ignored: [ + // "**/node_modules/**", + // "**/dist/**", + // "**/__snapshots__/**", + // ], + // }, }, logLevel: "error", optimizeDeps: { force: true }, @@ -314,7 +317,7 @@ async function testHMR(dir: string, config: FixtureConfig) { mode: "development", appType: "custom", logLevel: "error", - plugins: [markoPlugin(config.options)], + plugins: [markoPlugin(config.options), injectHmrEventsPlugin()], optimizeDeps: { force: true }, server: { port, @@ -405,21 +408,20 @@ async function testHMR(dir: string, config: FixtureConfig) { fs.writeFileSync(filePath, newContent); } - // Wait for HMR update to propagate to the browser. - // We watch for DOM changes on #app as a signal that HMR has taken effect. - await page - .waitForFunction( + // // Wait for HMR update to propagate to the browser. + // // We listen for Vite HMR events and watch for DOM changes on #app as a signal that HMR has taken effect. + + await Promise.any([ + page.evaluate(() => (window as any).__nextHmr), + page.waitForFunction( (prev) => { const app = document.getElementById("app"); return app && app.innerHTML !== prev; }, prevRawHtml, - { timeout: 10000 }, - ) - .catch(() => { - // Timeout is acceptable — the HMR update may not change the #app content, - // or the page may have been fully reloaded. - }); + { timeout: 5000 }, + ), + ]).catch(() => {}); const hmrHtml = await getHTML(); const changesDesc = hmrStep.changes diff --git a/src/__tests__/utils/inject-hmr-events-plugin.ts b/src/__tests__/utils/inject-hmr-events-plugin.ts new file mode 100644 index 0000000..a45c948 --- /dev/null +++ b/src/__tests__/utils/inject-hmr-events-plugin.ts @@ -0,0 +1,46 @@ +import type { Plugin } from "vite"; + +export default (): Plugin => { + return { + name: "inject-hmr-events", + enforce: "pre", + apply: "serve", + transform(source, id) { + if (id.endsWith("client-entry.marko")) { + return ( + source + + ` +if (import.meta.hot) { + import.meta.hot.on("vite:afterUpdate", () => { + requestAnimationFrame(() => { + window.__nextHmr.resolve(); + window.__nextHmr = createDeferred(); + }); + }); + import.meta.hot.on("vite:error", (err) => { + window.__nextHmr.reject(err); + window.__nextHmr = createDeferred(); + }); + + window.__nextHmr ||= createDeferred(); + + function createDeferred() { + let resolve; + let reject; + const deferred = new Promise((res, rej) => { + resolve = res; + reject = rej; + }) + deferred.resolve = resolve; + deferred.reject = reject; + return deferred; + } +} else { + window.__nextHmr ||= Promise.reject("import.meta.hot not defined"); +} +` + ); + } + }, + }; +}; diff --git a/src/index.ts b/src/index.ts index b3f939c..e53cbbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,7 @@ const virtualFiles = new Map< string, VirtualFile | DeferredPromise >(); +const virtualFilesForTemplate = new Map>(); const importTagReg = /^<([^>]+)>$/; const optionalWatchFileReg = /[\\/](?:([^\\/]+)\.)?(?:marko-tag.json|(?:style|component|component-browser)\.\w+)$/; @@ -146,8 +147,16 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { if (devServer) { const prev = virtualFiles.get(id); - if (isDeferredPromise(prev)) { - prev.resolve(virtualFile); + if (prev) { + if (isDeferredPromise(prev)) { + prev.resolve(virtualFile); + } else { + let files = virtualFilesForTemplate.get(normalizedFrom); + if (!files) { + virtualFilesForTemplate.set(normalizedFrom, (files = new Set())); + } + files.add(id); + } } } @@ -492,31 +501,43 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { baseConfig.cache!.clear(); clearScanCache(); - for (const mod of ctx.modules) { - if (mod.id && virtualFiles.has(mod.id)) { - virtualFiles.set(mod.id, createDeferredPromise()); - } - } + const modules = new Set(ctx.modules); + const fileName = normalizePath(ctx.file); // When a child tag template changes, parent templates that analyzed // it may need recompilation (e.g., input destructuring changes in // Tags API affect the parent's compiled output). - const fileName = normalizePath(ctx.file); - let extraModules: undefined | typeof ctx.modules; for (const [parent, files] of transformAnalyzedTags) { - if (parent !== fileName && files.has(fileName)) { + if (files.has(fileName)) { const mods = this.environment.moduleGraph.getModulesByFile(parent); if (mods) { - extraModules ||= []; for (const mod of mods) { - extraModules.push(mod); + modules.add(mod); + } + } + } + } + + // When a .marko file changes, its virtual dependencies (e.g., + // extracted CSS from style {} blocks) also need to be included + // in the HMR update so the browser receives the new content. + const virtualFileIds = virtualFilesForTemplate.get(fileName); + if (virtualFileIds) { + for (const id of virtualFileIds) { + if (!isDeferredPromise(virtualFiles.get(id))) { + virtualFiles.set(id, createDeferredPromise()); + } + const mods = this.environment.moduleGraph.getModulesByFile(id); + if (mods) { + for (const mod of mods) { + modules.add(mod); } } } } - if (extraModules) { - return [...ctx.modules, ...extraModules]; + if (modules.size !== ctx.modules.length) { + return [...modules]; } }, @@ -726,18 +747,15 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { return virtualFiles.get(id) || cachedSources.get(id) || null; }, async transform(source, rawId) { - const resolvedId = stripViteQueries(rawId); - const info = getMarkoFileInfo(resolvedId); - let id = - info?.kind === InternalFileKind.virtual - ? resolvedId - : info?.sourceId || resolvedId; + const id = stripViteQueries(rawId); + const info = getMarkoFileInfo(id); const isSSR = this.environment.name === "ssr"; + const isClientEntry = info?.kind === InternalFileKind.clientEntry; + const isServerEntry = info?.kind === InternalFileKind.serverEntry; - if (info?.kind === InternalFileKind.serverEntry) { - const fileName = id; + if (isServerEntry) { + const fileName = info.sourceId; let mainEntryData: string; - id = resolvedId; cachedSources.set(fileName, source); if (isBuild) { @@ -840,14 +858,14 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { const compiled = await compiler.compile( source, - id, + isClientEntry ? info.sourceId : id, isSSR ? isCJSModule(id, rootResolveFile) ? serverCJSConfig - : info?.kind === InternalFileKind.serverEntry + : isServerEntry ? serverEntryConfig : serverConfig - : info?.kind === InternalFileKind.clientEntry + : isClientEntry ? clientEntryConfig : clientConfig, ); @@ -855,13 +873,18 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { const { meta } = compiled; let { code } = compiled; - if (serverManifest && info?.kind === InternalFileKind.clientEntry) { + if (serverManifest && isClientEntry) { for (const assetId of serverManifest.ssrAssetIds) { code += `\nimport "${relativeImportPath(id, path.resolve(root, assetId))}";`; } } - if (!info && !isTest && devServer && !isTagsApi(meta.api)) { + if ( + (!info || isClientEntry) && + !isTest && + devServer && + !isTagsApi(meta.api) + ) { code += `\nif (import.meta.hot) import.meta.hot.accept(() => {});`; } @@ -870,6 +893,9 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { if (meta.analyzedTags) { const files = new Set(); transformAnalyzedTags.set(id, files); + if (isClientEntry) { + files.add(normalizePath(info.sourceId)); + } for (const file of meta.analyzedTags) { files.add(normalizePath(file)); } From d4fae7c47bd7aab036e7908b45bbbe2f435f5a66 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 13 Apr 2026 18:02:45 -0700 Subject: [PATCH 2/2] fix: handle client hot updates and necessary reloads --- .../__snapshots__/dev-hmr.expected.md | 2 +- .../__snapshots__/build.expected.md | 29 +++++++ .../__snapshots__/dev-hmr.expected.md | 60 +++++++++++++++ .../__snapshots__/dev.expected.md | 29 +++++++ .../isomorphic-server-only-hmr/dev-server.mjs | 43 +++++++++++ .../isomorphic-server-only-hmr/server.mjs | 16 ++++ .../src/components/class-component.marko | 20 +++++ .../src/components/implicit-component.marko | 10 +++ .../src/components/layout-component.marko | 15 ++++ .../isomorphic-server-only-hmr/src/index.js | 9 +++ .../src/template.marko | 10 +++ .../isomorphic-server-only-hmr/test.config.ts | 19 +++++ .../__snapshots__/dev-hmr.expected.md | 4 +- .../__snapshots__/dev-hmr.expected.md | 2 +- .../__snapshots__/dev-hmr.expected.md | 4 +- src/__tests__/main.test.ts | 52 ++++++++----- .../utils/inject-hmr-events-plugin.ts | 6 +- src/index.ts | 75 ++++++++++++++----- 18 files changed, 359 insertions(+), 46 deletions(-) create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/build.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev-hmr.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev.expected.md create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/dev-server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/server.mjs create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/class-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/implicit-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/layout-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/src/index.js create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/src/template.marko create mode 100644 src/__tests__/fixtures/isomorphic-server-only-hmr/test.config.ts diff --git a/src/__tests__/fixtures/isomorphic-basic-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-basic-hmr/__snapshots__/dev-hmr.expected.md index 6bf55cc..25150ef 100644 --- a/src/__tests__/fixtures/isomorphic-basic-hmr/__snapshots__/dev-hmr.expected.md +++ b/src/__tests__/fixtures/isomorphic-basic-hmr/__snapshots__/dev-hmr.expected.md @@ -21,7 +21,7 @@ await page.click("#clickable") ``` -# HMR 0 +# HMR 0 (No Reload) src/components/class-component.marko: "Clicks: ${state.clickCount}" → "Click count: ${state.clickCount}" ```diff diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/build.expected.md b/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/build.expected.md new file mode 100644 index 0000000..cd52ed6 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/build.expected.md @@ -0,0 +1,29 @@ +# Loading + +```html +

+ Hello +

+
+

+ Server text +

+
+ Mounted: true Clicks: 0 +
+
+``` + +# Step 0 +await page.click("#clickable") + +```diff +- Mounted: true Clicks: 0 ++ Mounted: true Clicks: 1 + +``` + diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev-hmr.expected.md new file mode 100644 index 0000000..920cb89 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev-hmr.expected.md @@ -0,0 +1,60 @@ +# Loading + +```html +

+ Hello +

+
+

+ Server text +

+
+ Mounted: true Clicks: 0 +
+
+``` + +# Step 0 +await page.click("#clickable") + +```diff +- Mounted: true Clicks: 0 ++ Mounted: true Clicks: 1 + +``` + +# HMR 0 (Reload) +src/template.marko: "

Hello

" → "

Goodbye

" + +```diff +- Hello ++ Goodbye +- Mounted: true Clicks: 1 ++ Mounted: true Clicks: 0 + +``` + +# HMR 0 Step 0 +await page.click("#clickable") + +```diff +- Mounted: true Clicks: 0 ++ Mounted: true Clicks: 1 + +``` + +# HMR 1 (Reload) +src/components/implicit-component.marko: "

Server text

" → "

Updated

" + +```diff +- Server text ++ Updated +- Mounted: true Clicks: 1 ++ Mounted: true Clicks: 0 + +``` + diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev.expected.md b/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev.expected.md new file mode 100644 index 0000000..cd52ed6 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/__snapshots__/dev.expected.md @@ -0,0 +1,29 @@ +# Loading + +```html +

+ Hello +

+
+

+ Server text +

+
+ Mounted: true Clicks: 0 +
+
+``` + +# Step 0 +await page.click("#clickable") + +```diff +- Mounted: true Clicks: 0 ++ Mounted: true Clicks: 1 + +``` + diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/dev-server.mjs b/src/__tests__/fixtures/isomorphic-server-only-hmr/dev-server.mjs new file mode 100644 index 0000000..293d792 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/dev-server.mjs @@ -0,0 +1,43 @@ +// In dev we'll start a Vite dev server in middleware mode, +// and forward requests to our http request handler. + +import { createRequire } from "module"; +import path from "path"; +import url from "url"; +import { createServer } from "vite"; + +// change to import once marko-vite is updated to ESM +const markoPlugin = createRequire(import.meta.url)("../../..").default; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const devServer = await createServer({ + root: __dirname, + appType: "custom", + logLevel: "warn", + plugins: [markoPlugin()], + optimizeDeps: { force: true }, + server: { + ws: false, + hmr: false, + middlewareMode: true, + watch: { + ignored: ["**/node_modules/**", "**/dist/**", "**/__snapshots__/**"], + }, + }, + build: { + assetsInlineLimit: 0, + }, +}); + +export default devServer.middlewares.use(async (req, res, next) => { + try { + const { handler } = await devServer.ssrLoadModule( + path.join(__dirname, "./src/index.js"), + ); + await handler(req, res, next); + } catch (err) { + devServer.ssrFixStacktrace(err); + return next(err); + } +}); diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/server.mjs b/src/__tests__/fixtures/isomorphic-server-only-hmr/server.mjs new file mode 100644 index 0000000..76060b9 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/server.mjs @@ -0,0 +1,16 @@ +// In production, simply start up the http server. +import { createServer } from "http"; +import path from "path"; +import serve from "serve-handler"; +import url from "url"; + +import { handler } from "./dist/index.mjs"; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const serveOpts = { public: path.resolve(__dirname, "dist") }; + +export default createServer(async (req, res) => { + await handler(req, res); + if (res.headersSent) return; + await serve(req, res, serveOpts); +}); diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/class-component.marko b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/class-component.marko new file mode 100644 index 0000000..9579382 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/class-component.marko @@ -0,0 +1,20 @@ +class { + onCreate() { + this.state = { + clickCount: 0, + mounted: false + }; + } + onMount() { + this.state.mounted = true; + } + + handleClick() { + this.state.clickCount++; + } +} + + + Mounted: ${state.mounted} + Clicks: ${state.clickCount} + diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/implicit-component.marko b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/implicit-component.marko new file mode 100644 index 0000000..df0a05d --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/implicit-component.marko @@ -0,0 +1,10 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Implicit Component"); + } +} + + +

Server text

+ + diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/layout-component.marko b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/layout-component.marko new file mode 100644 index 0000000..9a1c58e --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/components/layout-component.marko @@ -0,0 +1,15 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Layout Component"); + } +} + + + + + Hello World + + + <${input.renderBody}/> + + diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/src/index.js b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/index.js new file mode 100644 index 0000000..d3f5422 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/index.js @@ -0,0 +1,9 @@ +import template from "./template.marko"; + +export function handler(req, res) { + if (req.url === "/") { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + template.render({}, res); + } +} diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/src/template.marko b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/template.marko new file mode 100644 index 0000000..da11fa8 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/src/template.marko @@ -0,0 +1,10 @@ +style { + div { color: green } +} + + + +

Hello

+ + +
diff --git a/src/__tests__/fixtures/isomorphic-server-only-hmr/test.config.ts b/src/__tests__/fixtures/isomorphic-server-only-hmr/test.config.ts new file mode 100644 index 0000000..a757e8f --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-server-only-hmr/test.config.ts @@ -0,0 +1,19 @@ +export const ssr = true; +export async function steps() { + await page.click("#clickable"); +} +export const hmr = [ + { + changes: [["src/template.marko", "

Hello

", "

Goodbye

"]], + steps, + }, + { + changes: [ + [ + "src/components/implicit-component.marko", + "

Server text

", + "

Updated

", + ], + ], + }, +]; diff --git a/src/__tests__/fixtures/isomorphic-tags-api-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-tags-api-hmr/__snapshots__/dev-hmr.expected.md index ebb66cd..b3ec29c 100644 --- a/src/__tests__/fixtures/isomorphic-tags-api-hmr/__snapshots__/dev-hmr.expected.md +++ b/src/__tests__/fixtures/isomorphic-tags-api-hmr/__snapshots__/dev-hmr.expected.md @@ -6,7 +6,7 @@ ``` -# HMR 0 +# HMR 0 (Reload) src/tags/child-tag.marko: "Hello ${input.name}" → "Hi ${input.name}" ```diff @@ -15,7 +15,7 @@ src/tags/child-tag.marko: "Hello ${input.name}" → "Hi ${input.name}" ``` -# HMR 1 +# HMR 1 (Reload) src/tags/child-tag.marko: "Hi ${input.name}" → "Hey ${JSON.parse(JSON.stringify(input)).name}" ```diff diff --git a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md index 8e69280..1a2337c 100644 --- a/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md +++ b/src/__tests__/fixtures/isomorphic-tags-api-virtual-file-hmr/__snapshots__/dev-hmr.expected.md @@ -6,7 +6,7 @@ ``` -# HMR 0 +# HMR 0 (Reload) src/template.marko: "div { color: green }" → "div { color: blue }" ```diff diff --git a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md index 67055be..f901fca 100644 --- a/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md +++ b/src/__tests__/fixtures/isomorphic-virtual-file-hmr/__snapshots__/dev-hmr.expected.md @@ -12,7 +12,7 @@ ``` -# HMR 0 +# HMR 0 (No Reload) src/template.marko: "div { color: green }" → "div { color: blue }" (no change) @@ -26,7 +26,7 @@ await page.click("#clickable") ``` -# HMR 1 +# HMR 1 (No Reload) src/template.marko: "div { color: blue }" → "div { color: red }" (no change) diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index 6576f54..9237ea6 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -241,13 +241,6 @@ for (const fixture of fs.readdirSync(FIXTURES)) { port, hmr: false, watch: null, - // watch: { - // ignored: [ - // "**/node_modules/**", - // "**/dist/**", - // "**/__snapshots__/**", - // ], - // }, }, logLevel: "error", optimizeDeps: { force: true }, @@ -346,6 +339,11 @@ async function testHMR(dir: string, config: FixtureConfig) { }); } + let hmrReloads = 0; + function trackReloads() { + hmrReloads++; + } + try { await devServer.listen(); @@ -390,6 +388,8 @@ async function testHMR(dir: string, config: FixtureConfig) { prevRawHtml = await getRawHTML(); } + page.on("framenavigated", trackReloads); + // Run HMR steps. for (const [hmrIdx, hmrStep] of hmrSteps.entries()) { // Apply file changes. @@ -413,15 +413,21 @@ async function testHMR(dir: string, config: FixtureConfig) { await Promise.any([ page.evaluate(() => (window as any).__nextHmr), - page.waitForFunction( - (prev) => { - const app = document.getElementById("app"); - return app && app.innerHTML !== prev; - }, - prevRawHtml, - { timeout: 5000 }, - ), - ]).catch(() => {}); + page + .waitForFunction( + (prev) => { + const app = document.getElementById("app"); + return app && app.innerHTML !== prev; + }, + prevRawHtml, + { timeout: 5000 }, + ) + .catch(() => { + /* Timeout */ + }), + ]); + + await page.waitForTimeout(100); const hmrHtml = await getHTML(); const changesDesc = hmrStep.changes @@ -430,7 +436,17 @@ async function testHMR(dir: string, config: FixtureConfig) { `${file}: ${JSON.stringify(find)} → ${JSON.stringify(replace)}`, ) .join("\n"); - snapshot += `# HMR ${hmrIdx}\n${changesDesc}\n\n`; + + snapshot += `# HMR ${hmrIdx}`; + + if (hmrReloads) { + snapshot += ` (Reload${hmrReloads > 1 ? ` x${hmrReloads}` : ""})`; + hmrReloads = 0; + } else { + snapshot += ` (No Reload)`; + } + + snapshot += `\n${changesDesc}\n\n`; if (hmrHtml !== prevHtml) { snapshot += htmlSnapshot(hmrHtml, prevHtml); @@ -464,6 +480,8 @@ async function testHMR(dir: string, config: FixtureConfig) { for (const [filePath, content] of originalFiles) { fs.writeFileSync(filePath, content); } + page.off("framenavigated", trackReloads); + await devServer.close(); } } diff --git a/src/__tests__/utils/inject-hmr-events-plugin.ts b/src/__tests__/utils/inject-hmr-events-plugin.ts index a45c948..2aa727f 100644 --- a/src/__tests__/utils/inject-hmr-events-plugin.ts +++ b/src/__tests__/utils/inject-hmr-events-plugin.ts @@ -12,10 +12,8 @@ export default (): Plugin => { ` if (import.meta.hot) { import.meta.hot.on("vite:afterUpdate", () => { - requestAnimationFrame(() => { - window.__nextHmr.resolve(); - window.__nextHmr = createDeferred(); - }); + window.__nextHmr.resolve(); + window.__nextHmr = createDeferred(); }); import.meta.hot.on("vite:error", (err) => { window.__nextHmr.reject(err); diff --git a/src/index.ts b/src/index.ts index e53cbbc..057b023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,6 +87,7 @@ const virtualFiles = new Map< VirtualFile | DeferredPromise >(); const virtualFilesForTemplate = new Map>(); +const ssrTransformCache = new Map(); const importTagReg = /^<([^>]+)>$/; const optionalWatchFileReg = /[\\/](?:([^\\/]+)\.)?(?:marko-tag.json|(?:style|component|component-browser)\.\w+)$/; @@ -147,17 +148,15 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { if (devServer) { const prev = virtualFiles.get(id); - if (prev) { - if (isDeferredPromise(prev)) { - prev.resolve(virtualFile); - } else { - let files = virtualFilesForTemplate.get(normalizedFrom); - if (!files) { - virtualFilesForTemplate.set(normalizedFrom, (files = new Set())); - } - files.add(id); - } + if (isDeferredPromise(prev)) { + prev.resolve(virtualFile); } + + let files = virtualFilesForTemplate.get(normalizedFrom); + if (!files) { + virtualFilesForTemplate.set(normalizedFrom, (files = new Set())); + } + files.add(id); } virtualFiles.set(id, virtualFile); @@ -473,6 +472,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { entryIds.delete(fileName); transformWatchFiles.delete(fileName); transformAnalyzedTags.delete(fileName); + virtualFilesForTemplate.delete(fileName); } for (const [id, files] of transformWatchFiles) { @@ -496,7 +496,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { }); }, - hotUpdate(ctx) { + async hotUpdate(ctx) { compiler.taglib.clearCaches(); baseConfig.cache!.clear(); clearScanCache(); @@ -530,12 +530,43 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { const mods = this.environment.moduleGraph.getModulesByFile(id); if (mods) { for (const mod of mods) { + this.environment.moduleGraph.invalidateModule(mod); modules.add(mod); } } } } + if (linked && this.environment.name === "ssr") { + const previous = new Map(); + + for (const mod of modules) { + const code = ssrTransformCache.get(mod.id!); + if (code !== undefined) { + previous.set(mod, code); + } + } + + if (previous.size) { + let reload = false; + for (const [mod, prevCode] of previous) { + await this.environment.transformRequest(mod.id!); + if ( + prevCode !== ssrTransformCache.get(mod.id!) && + !devServer.environments.client.moduleGraph.getModulesByFile( + mod.id!, + ) + ) { + reload = true; + } + } + + if (reload) { + devServer.hot.send({ type: "full-reload" }); + } + } + } + if (modules.size !== ctx.modules.length) { return [...modules]; } @@ -834,6 +865,12 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { return null; } + const fileName = isClientEntry ? info.sourceId : id; + + if (devServer) { + virtualFilesForTemplate.delete(normalizePath(fileName)); + } + if (isSSR) { if (linked) { cachedSources.set(id, source); @@ -858,7 +895,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { const compiled = await compiler.compile( source, - isClientEntry ? info.sourceId : id, + fileName, isSSR ? isCJSModule(id, rootResolveFile) ? serverCJSConfig @@ -879,17 +916,17 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { } } - if ( - (!info || isClientEntry) && - !isTest && - devServer && - !isTagsApi(meta.api) - ) { + if (!isTest && devServer && !isTagsApi(meta.api)) { code += `\nif (import.meta.hot) import.meta.hot.accept(() => {});`; } if (devServer) { - transformWatchFiles.set(id, meta.watchFiles); + if (isSSR && !isServerEntry) { + ssrTransformCache.set(id, code); + } + if (!info) { + transformWatchFiles.set(id, meta.watchFiles); + } if (meta.analyzedTags) { const files = new Set(); transformAnalyzedTags.set(id, files);