Skip to content

Commit 768d1a7

Browse files
committed
Paper.
1 parent e24acf4 commit 768d1a7

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

site/public/blog/native-nodejs-hmr.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Implementing native Node.js hot modules (technical write up)
2+
3+
One of the key factors in rapid development is
4+
discarding as little state as possible.
5+
In Node.js, this means the new `--watch` flags
6+
are not that useful, since they throw everything away.
7+
The ideal is to simply invalidate module when it changes
8+
or when a module it depends on changes.
9+
This way, all imports and data are always fresh,
10+
but only partial module trees get re-evaluated.
11+
12+
Previously, immaculata did the same thing Vite does now:
13+
use the built-in `node:vm` functionality to create
14+
an ad hoc module system that sits on top of Node's,
15+
and glues these systems together using custom logic.
16+
This effectively creates second class modules.
17+
18+
The main drawbacks are that logic is duplicated and separated.
19+
Duplication happens in finding and loading files, parsing them,
20+
evaluating and storing their module objects, and gluing all these
21+
together with each other and with Node's own module system.
22+
And these systems are inherently separate, so that
23+
native Node module hooks will have no effect on the ad hoc system.
24+
25+
By adding module hooks using Node's built-in `node:module` module,
26+
it's now possible to implement "hot module" functionality natively.
27+
28+
First, we load source files from disk and keep them in memory.
29+
This won't hit memory limits for most projects and dev machines.
30+
To handle this need, we have a [FileTree] class,
31+
which does nothing other than load a file tree into memory,
32+
and optionally keep it up to date via [.watch()][watch],
33+
which returns an `EventEmitter` with a `filesUpdated` event.
34+
Node's native file watcher is now disk efficient,
35+
and returns all the information we need,
36+
so we don't need `chokidar` for this.
37+
38+
Next, we have the [useTree] dual-hook which does two key things.
39+
First, it implements a loader hook that returns the source `string`
40+
using `tree.files.get` instead of `fs.readFileSync`.
41+
Second, it implements a resolver hook that appends `?ver=${file.version}`
42+
to the URL of any given module.
43+
44+
What ties all of this together is the fact that
45+
the [FileTree] constructor and the [watch] method
46+
both set each file's `version` to `Date.now()`.
47+
This becomes an automatic query busting string,
48+
which works because Node internally uses URLs
49+
to represent all modules.
50+
51+
In practice, this means that you can import a module file initially,
52+
and import the same file again after the `filesUpdated` event,
53+
and either the cached module object will be returned,
54+
or the file will be re-evaluated if it was updated.
55+
56+
The one missing piece of this puzzle was dependency trees.
57+
Because module hooks are called during import,
58+
we can use this information to register dependencies,
59+
which is done internally within [FileTree].
60+
Each time a dependency of a module changes,
61+
the parent module itself also has its version updated.
62+
This works recursively, so that modules are always fresh,
63+
and updated even if a single deep dependency changes.
64+
65+
The code to use these hooks is relatively short and simple:
66+
67+
```ts
68+
import { FileTree } from "immaculata"
69+
import { useTree } from "immaculata/hooks.js"
70+
import { registerHooks } from 'node:module'
71+
72+
const tree = new FileTree('src', import.meta.dirname)
73+
registerHooks(useTree(tree))
74+
75+
const myModule = await import('src/myModule.js')
76+
// src/myModule.js is executed
77+
78+
const myModule2 = await import('src/myModule.js')
79+
// src/myModule.js is NOT executed a second time
80+
81+
tree.watch().on('filesUpdated', async () => {
82+
const myModule = await import('src/myModule.js')
83+
// src/myModule.js IS executed again iff invalidated
84+
})
85+
```
86+
87+
[FileTree]: ../api/filetree.md#filetree
88+
[watch]: ../api/filetree.md#watch
89+
[useTree]: ../api/module-hooks.md#usetree

0 commit comments

Comments
 (0)