feat(react-email): hot reloading around dynamic import

Merged
AC
5mo ago
+438-47

Fixes #3412.

When an email template loads files through a dynamic import() . for example, react-i18next registering translations via resourcesToBackend((lng, ns) => import(./messages/${lng}/${ns}.json)) . those files never entered the static dependency graph that email dev builds. The chokidar watcher therefore never saw them, and editing a translation JSON did not refresh the preview.

The user had to edit the template at the same time or restart the dev server.

This PR teaches the dev server's AST traversal to recognize dynamic import() calls, so the relevant directories are watched automatically, no flag, no configuration.

How it works

The dependency graph already parses every module with Babel and collects static import / require paths. Two cases are now also
handled:

  1. import('./literal-string') treated identically to a static import. The path is added to the graph as a regular file
    dependency.

  2. import(`./prefix/${expr}/...`) the leading static portion of the template literal (./prefix/) is captured as a glob
    directory dependency
    of the importing module. The directory is added to chokidar's watch set, and any file change inside it
    propagates to the importing module's dependents through the existing transitive-resolution path. The set of watched directories is
    reconciled on every graph update, so adding or removing a dynamic import while the dev server is running just works.

Guards in resolveGlobPrefixToDirectory keep the watch set focused:

  • bare module specifiers are skipped (import(`pkg/${x}`));
  • the importing module's own directory and its ancestors are skipped (import(`./${x}`) would otherwise reload everything on
    every change);
  • non-existent or non-directory targets are skipped.

Behavior

  • import(`./locales/${lng}.json`) → ./locales/ is watched automatically.
  • Works for any extension or no extension, the watcher is path-based, not content-aware. JSON, MDX, YAML, HTML, SVG, raw text,
    etc.
  • Static import 'x.json' continues to work exactly as before.

Summary by cubic

Auto-watches directories discovered from dynamic import() calls in react-email dev, so edits to runtime-loaded files (e.g., i18n JSON) refresh previews. Resolves tsconfig path aliases in template-literal imports with fallback; no flags or config needed.

  • Bug Fixes

    • Detect dynamic imports: import('./x') and import(./x) behave as static; import(./prefix/${...}) watches the leading ./prefix/; alias form import(@/prefix/${...}) resolves tsconfig path aliases and falls back to the first existing directory.
    • Changes in these directories map to the importing template (and its dependents), triggering reloads even when files aren’t in the static graph. The watch set is reconciled on graph updates; skips bare specifiers, the module’s own dir/ancestors, and missing/non-directory paths. Path checks handle edge cases (including root paths). Tests cover dynamic, alias, and alias-fallback cases.
  • Refactors

    • Renamed internal glob tracking to dynamicDependencyDirectories.
    • getImportedModules now returns { staticImports, dynamicImportPrefixes }; treats import() with a string literal or a template literal with no interpolation as static.