Skip to content

Resolve JS files and detect changes throughout their dependency tree.

Notifications You must be signed in to change notification settings

CyriacBr/modules-watcher

Repository files navigation

Modules Watcher

CI

This library provides a way to implement smart watch mode or hot module reloading for your javascript projects.
modules-watcher is standalone, and doesn't rely on any existing bundler or compile tool. It simply takes the paths and/or globs of the files you wish to "watch" (entries) and will walk through every one of their dependencies to appropriatly react when an entry change. Dependencies are resolved using these rules:

  • ESM imports
    • import [whatever] from 'bar'
    • import('bar')
  • CJS imports
    • require('foo')
  • (S)CSS imports
    • @import foo.css
    • @import "foo.css", url('bar.css')
  • Supports both node modules and relative imports
    • When importing a node module, modules-watcher will resolve it's entry file the same way require.resolve does.
  • Supports ~/

modules-watcher uses a custom parser to scan imports depending on the extension of the file being parsed. Furthermore, it comes with a cache, allowing you to get the changes between multiple usages. So you can detect changes that happened to your entries while you weren't actively watching.

Usage

First, instantiate the Watcher class using the setup factory.

import { ModulesWatcher } from 'modules-watcher';

const watcher = ModulesWatcher.setup({
  project: 'my-doc',
  projectRoot: 'absolute-path-to-project', // used for resolving `~/` and more
  globEntries: ['**/*.mdx'], // watch all files matching the globs
  entries: ['./config.ts'] // also watch specific files
});

Then, you can either:

  • Get the changes since the last time the program was executed.
  • Or actively watch any changes from now on.

Getting changes from last usage

makeChanges() will return changes based on the cache and the current checksum of the dependency tree of your entries.
This means that changes that occurred when modules-watcher wasn't running will be picked up.
The first time this method is called (there's no cache yet), every entry will be marked as Added.

const changes = watcher.makeChanges();
changes[0];
/**
 * {
 *    changeType: 'Added',
 *    entry: 'path/foo.mdx',
 * }
 **/

changes[1];
/**
 * {
 *    changeType: 'DepAdded',
 *    entry: 'path/foo.mdx',
 *    cause: {
 *      file: 'path/foo-component.js',
 *      state: 'Created',
 *    }
 *    tree: ['path/foo-component.js', 'path/foo.mdx'] 
 * }
 **/

Later on, modules-watcher will leverage the cache to detect entries that got added, deleted or removed in the meantime.

alterFooComponent();
addBarMdx();
deleteBazMdx();

const changes = watcher.makeChanges();
changes[0];
/**
 * {
 *    changeType: 'DepModified',
 *    entry: 'path/foo.mdx',
 *    cause: {
 *      file: 'path/foo-component.js',
 *      state: 'Modified',
 *    }
 *    tree: ['path/foo-component.js', 'path/foo.mdx'] 
 * }
 **/

changes[1];
/**
 * {
 *    changeType: 'Added',
 *    entry: 'path/bar.mdx',
 * }
 **/

changes[2];
/**
 * {
 *    changeType: 'Deleted',
 *    entry: 'path/baz.mdx',
 * }
 **/

Based on changeType and cause, it's possible to know if an entry was directly modified or if its dependencies are the ones that changed.
Naturally, if an entry is modified with a new import statement, you'll get a change with DepAdded for that entry.

Actively watching for changes

The method watch lets you watch in real-time any modification to your entries or their dependencies.
The callback to watch is called with the result of makeChanges every time a change is detected. Use stopWatch to stop watching.

watcher.watch((err, changes) => {
    if (!err && changes.some(change => change.entry === 'path/config.ts')) {
      fullReload();
    }
});

watcher.stopWatch();

Note that watch can't be called consecutively without stopWatch after each watch.

Configure the parser

When using setup, it's possible to specify how imports are parsed according to the extension of the file being read.

const watcher = ModulesWatcher.setup({
  ...,
  // default values:
  supportedPaths: {
    // parse ESM imports on these extensions
    esm: ["cjs", "esm", "js", "ts", "tsx", "jsx", "cts", "mts", "mdx"],
    // parse import() on these extensions
    dyn_esm: ["cjs", "esm", "js", "ts", "tsx", "jsx", "cts", "mts"],
    // parse require() on these extensions
    cjs: ["cjs", "esm", "js", "ts", "tsx", "jsx", "cts", "mts"],
    // parse CSS imports on these extensions
    css: ["css", "scss", "sass"]
  }
})

Other methods

getDirsToWatch: If you want to handle yourself the watching, this method gives you all the directory paths that need to be watched.

const paths = watcher.getDirsToWatch();
paths; // ['path/docs', 'path/docs/components', 'path/to/node-modules/react/dist']

getEntries: returns all entries with their dependencies.
Note that they don't necessarily come out ordered.

const entries = watcher.getEntries();
entries[0];
/**
 * {
 *    path: 'path/foo.mdx',
 *    deps: [
 *      'path/foo-component.js', 
 *      'path/to/node-modules/react/index.js'
 *    ] 
 * }
 **/

TBD.