# (ab?)using Node module hooks to speed up development
I wanted a much faster way to develop the front-end of my site.
And I didn't want to be tied down to any one way of doing anything.
So I made a bunch of orthogonal stuff. Half evolved into module hooks.
# Memory copy of file tree
I wanted to reduce disk reads when reading files.
So I made FileTree to load a file tree into memory and optionally keep it updated with tree.watch.
import { FileTree } from 'immaculata'
export const tree = new FileTree('site', import.meta.url)
// can now load files from memory rather than fs.readFileSync('site/style.css')
tree.files.get('/style.css')
// and react to file system changes
tree.watch().on('filesUpdated', changes => /* ... */)
tree.watch().on('moduleInvalidated', path => /* ... */)
# Hot module replacement/reloading
I wanted to develop modules and re-execute them without restarting the whole process.
So I created the useTree module hook that invalidates re-saved module files using cache busters.
import { FileTree, hooks } from 'immaculata'
import { registerHooks } from 'module'
export const tree = new FileTree('site', import.meta.url)
registerHooks(hooks.useTree(tree))
tree.watch().on('filesUpdated', dostuff)
dostuff()
function dostuff() {
import(tree.root + '/myfile.js')
// re-executes myfile.js if and only if it changes
// (or anything it imports, directly or indirectly)
}
# Module invalidation callbacks
I wanted to properly dispose singletons instead of restarting the whole process.
So I made onModuleInvalidated to run code when its being replaced with a newer version.
import * as ShikiMd from '@shikijs/markdown-it'
import type MarkdownIt from 'markdown-it'
import * as Shiki from 'shiki'
import { tree } from '../../static.ts'
const highlighter = await Shiki.createHighlighter({
themes: ['dark-plus'],
langs: ['tsx', 'typescript', 'json', 'yaml', 'bash'],
})
export function highlightCode(md: MarkdownIt) {
md.use(ShikiMd.fromHighlighter(highlighter, { theme: 'dark-plus' }))
}
tree.onModuleInvalidated(import.meta.url, () => {
highlighter.dispose()
})
# Incorrect file extensions
I wanted to be able to import foo.{ts,tsx,jsx}
but using the .js
file extension.
So I made a tryAltExts module hook that looks for .{ts,tsx,jsx}
when .js
isn't found.
import { hooks } from 'immaculata'
import { registerHooks } from 'module'
registerHooks(hooks.tryAltExts)
import('./foo.js') // now works even though only foo.tsx actually exists
# Compiling JSX
I wanted to import and run JSX files natively in Node.js.
So I made a compileJsx module loader that transforms JSX to JS with the function you give it.
import { hooks } from 'immaculata'
import { registerHooks } from 'module'
registerHooks(hooks.compileJsx((src, url) => /* ... use swc/ts/etc ... */))
# Remapping imports
I wanted to remap react/jsx-runtime
to ./my-jsx-impl.js
for experimentation.
So I made a mapImport module resolver hook that remaps imports by name.
import { hooks } from 'immaculata'
import { registerHooks } from 'module'
// experiment with your own JSX implementation
registerHooks(hooks.mapImport('react/jsx-runtime', import.meta.resolve('my-jsx-impl.js')))
// or use a highly optimized string-builder implementation
registerHooks(hooks.mapImport('react/jsx-runtime', 'immaculata/jsx-strings.js'))