Skip to content

HMR#79

Open
GreatAuk wants to merge 5 commits intouni-helper:mainfrom
GreatAuk:feat-HMR
Open

HMR#79
GreatAuk wants to merge 5 commits intouni-helper:mainfrom
GreatAuk:feat-HMR

Conversation

@GreatAuk
Copy link

@GreatAuk GreatAuk commented Dec 12, 2025

Description 描述

1. 监听 pages.json 中页面 layout 变化,针对变改的页面,重新触发 transform。

看之前的机制,更新 pages.json 后,vite 会触发所有文件的 transfrom。 其实没更新 layout 的页面并不需要,造成了浪费。
而且更新 vite 版本到 5.2.8(目前 uni app 官方模板)后,更新 pages.json 并不会触发所有文件的 transfrom。这样导致开发过程中,你动态更新 pages.json 并不生效,要重新运行 dev 命令才能生效

2. fix: 开发过程中,在layouts 目录下新增了 layout 文件,页面使用该 layout 无法生效

新增 layout 文件,无法生效,要重新运行 dev 命令才能生效。而且如果删除某个 layout, vite 还会报错无法找到当前文件

Linked Issues 关联的 Issues

Additional context 额外上下文

Summary by CodeRabbit

  • New Features
    • Automatic page reloads when layout configurations change during development to reduce manual refreshes.
    • Targeted reloads for only affected page files (including Vue/NVue) to minimize unnecessary restarts and speed up feedback.
    • Smarter watcher behavior that ignores irrelevant events and skips reloads when no layout changes are detected.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 12, 2025

Walkthrough

Context now tracks a resolved layouts directory, adds layoutDirPath and reloadLayouts(), and enhances the pages.json watcher to diff layout fields, invalidate/reload affected .vue/.nvue page modules via a new invalidateAndReload util and to reload layouts on layout file add/unlink events. scanLayouts signature and behavior were simplified to accept a directory path directly.

Changes

Cohort / File(s) Summary
Context & watcher
src/context.ts
Adds layoutDirPath and reloadLayouts() to Context; wires setupViteServer; updates pages.json watcher to capture previous pages, diff layout fields, check for corresponding .vue/.nvue files, call invalidateAndReload for changed pages, and watch layout add/unlink events to reload layouts. Adds guarded template access for layout detection.
Module invalidation utility
src/utils.ts
Adds exported invalidateAndReload(filePath: string, server?: ViteDevServer) and isLayoutFile(filePath: string, layoutDirPath: string); resolves module id, invalidates module in Vite module graph, triggers dev-server reload when available, and returns boolean success with logging.
Layout scanner
src/scan.ts
Changes scanLayouts signature to scanLayouts(dir: string); removes cwd resolution and uses provided directory as the glob cwd; simplifies path/resolve usage and removed unused imports.

Sequence Diagram(s)

sequenceDiagram
    participant FS as File System
    participant Context as Context (pages watcher)
    participant Scan as scanLayouts
    participant Utils as invalidateAndReload
    participant Vite as ViteDevServer / ModuleGraph

    Note over Context: pages.json change detected
    Context->>FS: read cached previous pages.json
    Context->>FS: load new pages.json
    Context->>Context: diff layout fields (old vs new)
    alt layouts changed for page(s)
        Context->>FS: check existence of `page.vue` / `page.nvue`
        Context->>Utils: call invalidateAndReload(filePath, server)
        Utils->>Vite: resolve module id / find module in graph
        alt module found
            Utils->>Vite: invalidate module
            Utils->>Vite: trigger dev-server reload
            Vite-->>Utils: reload acknowledged
            Utils-->>Context: return success
        else module missing
            Utils-->>Context: warn & return false
        end
    else no layout changes
        Context-->>Context: no action
    end

    Note over Context,Scan: layout file add/unlink
    FS->>Context: file add/unlink event
    Context->>Scan: call reloadLayouts() -> scanLayouts(layoutDirPath)
    Scan-->>Context: updated layout list (virtual module updated)
    Context->>Vite: invalidate main entry modules to re-run imports
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review layout-diff logic and edge cases in src/context.ts (comparison criteria, optional chaining).
  • Verify invalidateAndReload correctness with Vite module resolution, invalidation, and reload sequencing in src/utils.ts.
  • Confirm scanLayouts contract change and callers use the new signature.

Suggested reviewers

  • KeJunMao
  • skiyee

Poem

🐇
I hop where layout files rearrange,
I nudge the graph and watch things change.
A blink, a reload — modules gleam,
New pages hop into the stream! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'HMR' is vague and does not clearly convey the main change, which is implementing Hot Module Reload logic to detect layout changes in pages.json and re-trigger transforms for affected pages. Consider revising the title to be more descriptive, such as 'Add HMR for layout changes in pages.json' or 'Detect and reload layout changes during development', to better reflect the specific functionality being added.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/utils.ts (1)

97-122: Clarify whether filePath is a URL or filesystem path (current code assumes URL)
getModuleByUrl(filePath) implies callers must pass a Vite module URL like /src/foo.vue (not an absolute filesystem path). Naming it url (and optionally normalizing) avoids “module not found” surprises.

-// 使模块失效并重新加载
-export async function invalidateAndReload(filePath: string, server?: ViteDevServer) {
+// 使模块失效并重新加载(url 应为 Vite 模块 URL,如 /src/pages/index.vue)
+export function invalidateAndReload(url: string, server?: ViteDevServer) {
   if (!server) {
-    console.error('Vite server not available.')
+    console.error('[vite-plugin-uni-layouts] Vite server not available.')
     return false
   }
 
-  const module = await server.moduleGraph.getModuleByUrl(filePath)
+  const normalizedUrl = normalizePath(url)
+  const module = server.moduleGraph.getModuleByUrl(normalizedUrl)
 
   if (module) {
     server.moduleGraph.invalidateModule(module)
     server.reloadModule(module)
     return true
   }
   else {
-    console.warn(`[vite-plugin-uni-layouts] ❌ Module not found in module graph: ${filePath}`)
+    console.warn(`[vite-plugin-uni-layouts] ❌ Module not found in module graph: ${normalizedUrl}`)
     return false
   }
 }
src/context.ts (1)

50-70: Optional: avoid callback-style fs.access + fire-and-forget async calls
Not a blocker, but fs.promises.access + await would make ordering/error handling clearer (and makes it easier to await invalidateAndReload if you keep it async).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ccf808 and 31332cf.

📒 Files selected for processing (2)
  • src/context.ts (3 hunks)
  • src/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/context.ts (1)
src/utils.ts (2)
  • loadPagesJson (28-46)
  • invalidateAndReload (98-122)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/context.ts (1)

51-70: Path resolution is incorrect and will fail in many environments.

This issue was flagged in a previous review and remains unresolved. The current approach has two problems:

  1. resolve('/src', ...) on Unix resolves to an absolute system path /src/..., not a Vite-relative URL
  2. resolve('src', ...) for fs.access resolves relative to process.cwd(), which may differ from Vite's root

Derive paths from this._server.config.root for correctness:

         if (this._server && changedPages.length > 0) {
+          const root = this._server.config.root
           for (const page of changedPages) {
-            // 使 .vue 文件失效, 如果存在的话
-            const pagePathVue = normalizePath(
-              resolve('/src', `${page.path}.vue`),
-            )
-            fs.access(resolve('src', `${page.path}.vue`), fs.constants.F_OK, (err) => {
-              if (!err)
-                invalidateAndReload(pagePathVue, this._server)
-            })
-
-            // 使 .nvue 文件失效, 如果存在的话
-            const pagePathNv = normalizePath(
-              resolve('/src', `${page.path}.nvue`),
-            )
-            fs.access(resolve('src', `${page.path}.nvue`), fs.constants.F_OK, (err) => {
-              if (!err)
-                invalidateAndReload(pagePathNv, this._server)
-            })
+            const candidates = [`${page.path}.vue`, `${page.path}.nvue`]
+            for (const ext of candidates) {
+              const abs = resolve(root, 'src', ext)
+              fs.access(abs, fs.constants.F_OK, (err) => {
+                if (err)
+                  return
+                // Vite expects URLs relative to root with leading /
+                const url = normalizePath(`/src/${ext}`)
+                invalidateAndReload(url, this._server)
+              })
+            }
           }
         }
🧹 Nitpick comments (2)
src/context.ts (2)

39-48: Path-based layout comparison looks correct; consider tightening the path check.

The Map-based comparison by page.path correctly addresses the previous issue with index-based comparison. However, path.includes('pages.json') is a loose check that could match unrelated files like custom-pages.json.

-      if (path.includes('pages.json')) {
+      if (path.endsWith(this.pageJsonPath) || path.endsWith('pages.json')) {

57-69: Consider using fs.promises.access for cleaner async flow.

The callback-based approach works but using promises would be more consistent with modern async patterns and easier to reason about, especially if error handling needs to be added later.

+import { access, constants } from 'node:fs/promises'
// Then in the handler:
-            fs.access(resolve('src', `${page.path}.vue`), fs.constants.F_OK, (err) => {
-              if (!err)
-                invalidateAndReload(pagePathVue, this._server)
-            })
+            try {
+              await access(absPath, constants.F_OK)
+              await invalidateAndReload(viteUrl, this._server)
+            }
+            catch {
+              // File doesn't exist, skip
+            }

Note: This would require making the watcher callback async.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31332cf and acd8a29.

📒 Files selected for processing (1)
  • src/context.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/context.ts (1)
src/utils.ts (2)
  • loadPagesJson (28-46)
  • invalidateAndReload (98-122)
🔇 Additional comments (1)
src/context.ts (1)

1-2: LGTM!

The new imports for path resolution, filesystem access, and the invalidateAndReload utility are appropriate for the HMR functionality being added.

Also applies to: 13-13

@GreatAuk
Copy link
Author

@KeJunMao 麻烦 review 下

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/context.ts (1)

58-76: Hardcoded '/src' module URL + mixed path bases likely breaks for non-default Vite roots / non-src layouts (and on Windows).
You check existence via resolve(viteRoot, 'src', ...), but invalidate via resolve('/src', ...), which can point at the wrong URL for moduleGraph.getModuleByUrl(). Also, invalidateAndReload() is async but invoked fire-and-forget from a callback—add error handling to avoid silent failures.

Suggested rewrite (derive URL from server.config.root, and handle both .vue/.nvue uniformly):

-      const viteRoot = this._server!.config.root
-
-      if (!this._server || !changedPages.length)
+      if (!this._server || !changedPages.length)
         return
+      const viteRoot = this._server.config.root

       // 失效对应的模块,触发 transform
       for (const page of changedPages) {
-        // 使 .vue 文件失效, 如果存在的话
-        const pagePathVue = normalizePath(
-          resolve('/src', `${page.path}.vue`),
-        )
-        fs.access(resolve(viteRoot, 'src', `${page.path}.vue`), fs.constants.F_OK, (err) => {
-          if (!err)
-            invalidateAndReload(pagePathVue, this._server)
-        })
-
-        // 使 .nvue 文件失效, 如果存在的话
-        const pagePathNv = normalizePath(
-          resolve('/src', `${page.path}.nvue`),
-        )
-        fs.access(resolve(viteRoot, 'src', `${page.path}.nvue`), fs.constants.F_OK, (err) => {
-          if (!err)
-            invalidateAndReload(pagePathNv, this._server)
-        })
+        for (const ext of ['.vue', '.nvue'] as const) {
+          const abs = resolve(viteRoot, 'src', `${page.path}${ext}`)
+          fs.access(abs, fs.constants.F_OK, (err) => {
+            if (err)
+              return
+            // Vite URLs are rooted at `/` and relative to `server.config.root`
+            const url = normalizePath(`/${abs.slice(normalizePath(viteRoot).length + 1)}`)
+            void invalidateAndReload(url, this._server).catch((e) => {
+              console.warn('[vite-plugin-uni-layouts] invalidateAndReload failed:', e)
+            })
+          })
+        }
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8deeec0 and 86d1388.

📒 Files selected for processing (1)
  • src/context.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/context.ts (1)
src/utils.ts (2)
  • loadPagesJson (28-46)
  • invalidateAndReload (98-122)

@pull-request-size pull-request-size bot added size/L and removed size/M labels Dec 12, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/scan.ts (1)

7-12: Test bug at test/scanLayouts.test.ts:7: scanLayouts() accepts only one parameter but test passes two

The function signature is scanLayouts(dir: string), but the test calls scanLayouts('src/layouts', cwd) passing a second argument that the function ignores. Update the test to pass the full resolved path as the single argument, similar to how production code does it in src/context.ts: scanLayouts(resolve(__dirname, 'fixtures', 'src/layouts')).

The production call sites are correct—Context properly passes this.layoutDirPath = resolve(options.cwd, options.layoutDir) at lines 33–34 and 103 in src/context.ts, ensuring absolute paths are used.

♻️ Duplicate comments (1)
src/context.ts (1)

47-85: Watcher path match + _server! ordering + hardcoded /src invalidation URL are fragile (Windows + non-src roots)

  • path.includes(this.pageJsonPath) can misfire; chokidar typically passes absolute paths—prefer normalized equality against the resolved pages.json absolute path.
  • const viteRoot = this._server!.config.root is read before the null-guard.
  • resolve('/src', ...) is not a safe way to construct a Vite URL (notably on Windows it can become C:/src/...), and it hardcodes /src.

Suggested shape (illustrative—adapt to your existing pageJsonPath/root decisions):

 watcher.on('change', async (path) => {
-  if (!path.includes(this.pageJsonPath))
+  const absPagesJson = resolve(this._server?.config.root ?? this.options.cwd, this.pageJsonPath)
+  if (normalizePath(path) !== normalizePath(absPagesJson))
     return
 
   const prePages = this.pages
   this.pages = loadPagesJson(this.pageJsonPath, this.options.cwd)
@@
-  const viteRoot = this._server!.config.root
-
-  if (!this._server || !changedPages.length)
+  if (!this._server || !changedPages.length)
     return
+  const viteRoot = this._server.config.root
 
   for (const page of changedPages) {
-    const pagePathVue = normalizePath(resolve('/src', `${page.path}.vue`))
-    fs.access(resolve(viteRoot, 'src', `${page.path}.vue`), fs.constants.F_OK, (err) => {
+    const absVue = resolve(viteRoot, 'src', `${page.path}.vue`)
+    fs.access(absVue, fs.constants.F_OK, (err) => {
       if (!err)
-        invalidateAndReload(pagePathVue, this._server)
+        invalidateAndReload(normalizePath(`/${relative(viteRoot, absVue)}`), this._server)
     })
#!/bin/bash
# Verify how pages.json path is set/resolved, and how invalidateAndReload is called.
rg -n --type=ts -C3 "pageJsonPath\\s*=|setupWatcher\\(|watcher\\.on\\('change'|invalidateAndReload\\(" src/context.ts

# Verify virtual module resolveId/load id (is it prefixed with \\0?)
rg -n --type=ts -C3 "virtualModuleId|resolveId\\s*\\(|load\\s*\\(|\\x00virtual:|\\\\0virtual:" src
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86d1388 and 4903b65.

📒 Files selected for processing (3)
  • src/context.ts (4 hunks)
  • src/scan.ts (1 hunks)
  • src/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/context.ts (4)
src/types.ts (3)
  • ResolvedOptions (20-20)
  • Page (29-32)
  • Layout (22-27)
src/scan.ts (1)
  • scanLayouts (7-48)
src/utils.ts (3)
  • loadPagesJson (28-46)
  • invalidateAndReload (98-122)
  • isLayoutFile (125-129)
src/constant.ts (1)
  • virtualModuleId (1-1)

@GreatAuk GreatAuk changed the title feat: 监听 pages.json 中页面 layout 变化,针对变改的页面,重新触发 transform HMR Dec 12, 2025
@GreatAuk
Copy link
Author

@FliPPeDround 麻烦 review 下

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant