Skip to content

Double browser reload when using external build tool (esbuild) with assets.folder #459

Description

@schalkneethling

Description

When using miyagi with an external build tool (esbuild) that watches source files and outputs to a folder configured in assets.folder, miyagi triggers two browser reloads for every file change instead of one.

Environment

  • miyagi version: 4.x (with Twing)
  • Node.js: 24.1.0
  • OS: macOS

Setup

Our project has:

  • Source files: src/components/ (templates, mocks, CSS, JS)
  • Build output: build/assets/ (esbuild bundles CSS/JS here)
  • miyagi config:
assets: {
  css: ['build/assets/css/common.css', ...],
  js: [{src: 'build/assets/js/common.js', type: 'module'}, ...],
  folder: ['src/fonts', 'build/assets'],
},
components: {
  folder: 'src/components',
}

We run miyagi start and esbuild --watch in parallel.

Steps to Reproduce

  1. Configure miyagi with assets.folder pointing to esbuild's output directory
  2. Run miyagi and esbuild watch mode in parallel
  3. Edit a CSS file in src/components/
  4. Observe terminal output

Expected Behavior

One browser reload after the file change is processed.

Actual Behavior

Two browser reloads occur:

  1. First reload when miyagi detects the source file change in src/components/
  2. Second reload ~1-5 seconds later when miyagi detects esbuild's output in build/assets/

Terminal output shows:

A file has been changed, miyagi is updating the state.
[watch] build started (change: "src/components/.../file.css")
[watch] build finished
A file has been changed, miyagi is updating the state.  <-- second detection
Reloading browser window!
Updating done!
Reloading browser window!  <-- second reload
Updating done!

Root Cause Analysis

Looking at lib/init/watcher.js, miyagi watches:

const foldersToWatch = [
  ...assets.folder.map((f) => path.join(global.config.assets.root, f)),
  ...assets.css...,
  ...assets.js...,
];

if (components.folder) {
  foldersToWatch.push(components.folder);
}

Both src/components AND build/assets are watched. When a source file changes:

  1. miyagi detects it → triggers reload
  2. esbuild rebuilds to build/assets/
  3. miyagi detects the build output → triggers another reload

The components.ignores filter doesn't help because:

  1. It only accepts strings (functions cause sanitizePath.startsWith is not a function error)
  2. Glob patterns like ** cause regex errors
  3. Simple strings don't match full paths properly with anymatch

Suggested Solutions

Option 1: Separate "serve" from "watch"

Add a config option to serve static files WITHOUT watching them:

assets: {
  folder: ['build/assets'],
  watchFolder: false,  // NEW: serve but don't watch
}

Option 2: Add watchIgnores config

Add a dedicated ignore pattern for the watcher that supports globs/functions:

watcher: {
  ignores: ['**/build/**'],  // or: (path) => path.includes('/build/')
}

Option 3: File type filtering for components folder

Allow specifying which file types to watch in the components folder:

components: {
  folder: 'src/components',
  watchPatterns: ['**/*.twig', '**/*.yaml', '**/*.md'],  // Only watch these
}

Option 4: Increase debounce timeout

The current 10ms debounce in the watcher is too short to batch the source change with the build output change. Increasing it to ~1000-1500ms would help, though this is a workaround rather than a fix.

Additional Issue: Spurious "Updating configuration" events

During debugging, we also observed multiple "Updating configuration..." messages even when only component files were changed (not .miyagi.js). This appears to be related to fs.watch emitting spurious events on macOS. The config file watcher might benefit from debouncing or using a more robust file watching approach.

Workaround Attempts That Failed

  1. Adding "build" or "build/assets" to components.ignores - doesn't match paths correctly
  2. Using glob patterns like "**/build/**" - causes regex errors
  3. Using functions in ignores - causes sanitizePath.startsWith is not a function

Happy to provide more details or help with a fix. This is a common setup for projects that use bundlers alongside miyagi.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions