Skip to content

Commit

Permalink
feat: add hot-hook/register entrypoint loader (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-R44 authored Apr 9, 2024
1 parent 4d1a480 commit fa6ed2e
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 76 deletions.
30 changes: 30 additions & 0 deletions .changeset/few-hotels-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"hot-hook": patch
---

This PR adds a new way of configuring hot-hook. This allows you to configure hot-hook without having to modify your codebase.

We introduce a new `hot-hook/register` entrypoint that can be used with Node.JS's `--import` flag. By using this method, the Hot Hook hook will be loaded at application startup without you needing to use `hot.init` in your codebase. It can be used as follows:

```bash
node --import=hot-hook/register ./src/index.js
```

Be careful if you also use a loader to transpile to TS (`ts-node` or `tsx`), hot-hook must be placed in the second position, after the TS loader :

```bash
node --import=tsx --import=hot-hook/register ./src/index.ts
```

To configure boundaries and other files, you'll need to use your application's `package.json` file, in the `hot-hook` key. For example:

```jsonc
// package.json
{
"hot-hook": {
"boundaries": [
"./src/controllers/**/*.tsx"
]
}
}
```
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,36 @@ The library is designed to be very light and simple. It doesn't perform any dark
pnpm add hot-hook
```

Once installed, you need to add the following code as early as possible in your NodeJS application.
### Initialization

You have two ways to initialize Hot Hook in your application.

### Using `--import` flag

You can use the `--import` flag to load the Hot Hook hook at application startup without needing to use `hot.init` in your codebase. If you are using a loader to transpile to TS (`ts-node` or `tsx`), Hot Hook must be placed in the second position, after the TS loader otherwise it won't work.

```bash
node --import=tsx --import=hot-hook/register ./src/index.ts
```

To configure boundaries and other files, you'll need to use your application's `package.json` file, in the `hot-hook` key. For example:

```json
// package.json
{
"hot-hook": {
"boundaries": [
"./src/controllers/**/*.tsx"
]
}
}
```

Or you can still use the `import.meta.hot?.boundary` attribute in your code to specify which files should be hot reloadable.

### Using `hot.init`

You need to add the following code as early as possible in your NodeJS application.

```ts
import { hot } from 'hot-hook'
Expand Down Expand Up @@ -197,6 +226,9 @@ It's quite simple. However, we ship a process manager with Hot Hook. See the doc
await import('./users_controller.js', import.meta.hot?.boundary)
```

> [!TIP]
> One important thing to note is, ONLY dynamic imports can be hot reloadable. Static imports will not be hot reloadable so don't declare them as boundaries. Read more about this [here](#esm-cache-busting).
By importing a module this way, you are essentially creating a kind of boundary. This module and all the modules imported by it will be hot reloadable.

Let's take a more complete example. Essentially, Hot Hook has a very simple algorithm to determine whether the file you just edited is hot reloadable or not.
Expand Down
8 changes: 0 additions & 8 deletions examples/fastify/bin/start.ts

This file was deleted.

9 changes: 7 additions & 2 deletions examples/fastify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"dev:tsnode": "hot-runner --node-args=\"--import=./tsnode.esm.js\" bin/start.ts",
"dev:tsx": "hot-runner --node-args=\"--import=tsx\" bin/start.ts"
"dev:tsnode": "hot-runner --node-args=--import=./tsnode.esm.js --node-args=--import=hot-hook/register src/index.ts",
"dev:tsx": "hot-runner --node-args=--import=tsx --node-args=--import=hot-hook/register src/index.ts"
},
"devDependencies": {
"@hot-hook/runner": "workspace:*",
Expand All @@ -16,5 +16,10 @@
},
"dependencies": {
"fastify": "^4.26.2"
},
"hot-hook": {
"boundaries": [
"./services/**/*.ts"
]
}
}
15 changes: 4 additions & 11 deletions examples/fastify/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import Fastify from 'fastify'

const fastify = Fastify({
logger: {
transport: {
target: 'pino-pretty',
options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname' },
},
},
})
const fastify = Fastify({ logger: { transport: { target: 'pino-pretty' } } })

fastify.get('/', async (request, reply) => {
const { PostsService } = await import('./services/posts_service.js', { with: { hot: 'true' } })
fastify.get('/', async (_, __) => {
const { PostsService } = await import('./services/posts_service.js')
return new PostsService().getPosts()
})

/**
* This route is totally optional and can be used to visualize
* your dependency graph in a browser.
*/
fastify.get('/dump-viewer', async (request, reply) => {
fastify.get('/dump-viewer', async (_, reply) => {
const { dumpViewer } = await import('@hot-hook/dump-viewer')

reply.header('Content-Type', 'text/html; charset=utf-8')
Expand Down
2 changes: 1 addition & 1 deletion examples/fastify/src/services/posts_service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { uppercasePostTitles } from '../helpers/posts'
import { uppercasePostTitles } from '../helpers/posts.js'

export class PostsService {
/**
Expand Down
4 changes: 0 additions & 4 deletions examples/hono/bin/start.ts

This file was deleted.

9 changes: 7 additions & 2 deletions examples/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"dev:tsnode": "hot-runner --node-args=\"--import=./tsnode.esm.js\" bin/start.ts",
"dev:tsx": "hot-runner --node-args=\"--import=tsx\" bin/start.ts"
"dev:tsnode": "hot-runner --node-args=--import=./tsnode.esm.js --node-args=--import=hot-hook/register src/index.tsx",
"dev:tsx": "hot-runner --node-args=--import=tsx --node-args=--import=hot-hook/register src/index.tsx"
},
"devDependencies": {
"hot-hook": "workspace:*",
Expand All @@ -14,5 +14,10 @@
"dependencies": {
"@hono/node-server": "^1.9.0",
"hono": "^4.1.4"
},
"hot-hook": {
"boundaries": [
"./views/**/*.tsx"
]
}
}
2 changes: 1 addition & 1 deletion examples/hono/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ console.log('Ready to serve requests')
const app = new Hono()

app.get('/', async (c) => {
const { Home } = await import('./views/home.js', { with: { hot: 'true' } })
const { Home } = await import('./views/home.js')
return c.html(<Home />)
})

Expand Down
7 changes: 6 additions & 1 deletion examples/node_http_basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
"private": true,
"type": "module",
"scripts": {
"dev:tsnode": "hot-runner --node-args=\"--import=./tsnode.esm.js\" bin/start.ts"
"dev:tsnode": "hot-runner --node-args=--import=./tsnode.esm.js --node-args=--import=hot-hook/register src/index.ts"
},
"devDependencies": {
"hot-hook": "workspace:*",
"@hot-hook/runner": "workspace:*"
},
"hot-hook": {
"boundaries": [
"./app.ts"
]
}
}
6 changes: 3 additions & 3 deletions examples/node_http_basic/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as http from 'node:http'

const server = http.createServer(async (request, response) => {
const app = await import('./app.js', import.meta.hot?.boundary)
const app = await import('./app.js')
app.default(request, response)
})

server.listen(8080)
server.listen(3000)

console.log('Server running at http://localhost:8080/')
console.log('Server running at http://localhost:3000/')
5 changes: 3 additions & 2 deletions packages/hot_hook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"exports": {
".": "./build/src/hot.js",
"./loader": "./build/src/loader.js",
"./runner": "./build/src/runner.js",
"./register": "./build/src/register.js",
"./import-meta": {
"types": "./import-meta.d.ts"
}
Expand All @@ -33,7 +33,8 @@
"dependencies": {
"chokidar": "^3.6.0",
"fast-glob": "^3.3.2",
"picomatch": "^4.0.2"
"picomatch": "^4.0.2",
"read-package-up": "^11.0.0"
},
"author": "Julien Ripouteau <julien@ripouteau.com>",
"license": "MIT",
Expand Down
8 changes: 6 additions & 2 deletions packages/hot_hook/src/dependency_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ export default class DependencyTree {
#tree!: FileNode
#pathMap: Map<string, FileNode> = new Map()

constructor(options: { root: string }) {
constructor(options: { root?: string }) {
if (options.root) this.addRoot(options.root)
}

addRoot(path: string) {
this.#tree = {
version: 0,
parents: null,
reloadable: false,
path: options.root,
path: path,
dependents: new Set(),
dependencies: new Set(),
}
Expand Down
43 changes: 29 additions & 14 deletions packages/hot_hook/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,34 @@ import DependencyTree from './dependency_tree.js'
import { InitializeHookOptions } from './types.js'

export class HotHookLoader {
#projectRoot: string
#options: InitializeHookOptions
#projectRoot!: string
#messagePort?: MessagePort
#watcher: chokidar.FSWatcher
#pathIgnoredMatcher: Matcher
#watcher!: chokidar.FSWatcher
#pathIgnoredMatcher!: Matcher
#dependencyTree: DependencyTree
#hardcodedBoundaryMatcher: Matcher
#hardcodedBoundaryMatcher!: Matcher

constructor(options: InitializeHookOptions) {
this.#projectRoot = dirname(options.root)
this.#options = options
this.#messagePort = options.messagePort

this.#watcher = this.#createWatcher().add(options.root)
this.#pathIgnoredMatcher = new Matcher(this.#projectRoot, options.ignore)
this.#hardcodedBoundaryMatcher = new Matcher(this.#projectRoot, options.boundaries)
if (options.root) this.#initialize(options.root)

this.#dependencyTree = new DependencyTree({ root: options.root })
this.#messagePort?.on('message', (message) => this.#onMessage(message))
}

/**
* Initialize the class with the provided root path.
*/
#initialize(root: string) {
this.#projectRoot = dirname(root)
this.#watcher = this.#createWatcher().add(root)
this.#pathIgnoredMatcher = new Matcher(this.#projectRoot, this.#options.ignore)
this.#hardcodedBoundaryMatcher = new Matcher(this.#projectRoot, this.#options.boundaries)
}

/**
* When a message is received from the main thread
*/
Expand Down Expand Up @@ -153,13 +162,19 @@ export class HotHookLoader {
}

const resultPath = fileURLToPath(resultUrl)
const parentPath = fileURLToPath(parentUrl)

const isHardcodedBoundary = this.#hardcodedBoundaryMatcher.match(resultPath)
const reloadable = context.importAttributes.hot === 'true' ? true : isHardcodedBoundary
const isRoot = !parentUrl
if (isRoot) {
this.#dependencyTree.addRoot(resultPath)
this.#initialize(resultPath)
return result
} else {
const parentPath = fileURLToPath(parentUrl)
const isHardcodedBoundary = this.#hardcodedBoundaryMatcher.match(resultPath)
const reloadable = context.importAttributes.hot === 'true' ? true : isHardcodedBoundary

this.#dependencyTree.addDependency(parentPath, { path: resultPath, reloadable })
this.#dependencyTree.addDependent(resultPath, parentPath)
this.#dependencyTree.addDependency(parentPath, { path: resultPath, reloadable })
this.#dependencyTree.addDependent(resultPath, parentPath)
}

if (this.#pathIgnoredMatcher.match(resultPath)) {
return result
Expand Down
17 changes: 17 additions & 0 deletions packages/hot_hook/src/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { resolve } from 'node:path'
import { hot } from './hot.js'
import { readPackageUp } from 'read-package-up'

const pkgJson = await readPackageUp()
if (!pkgJson) {
throw new Error('Could not find package.json')
}

const { packageJson, path: packageJsonPath } = pkgJson
const hotHookConfig = packageJson['hot-hook']

await hot.init({
root: hotHookConfig?.root ? resolve(packageJsonPath, packageJson['hot-hook'].root) : undefined,
boundaries: packageJson['hot-hook']?.boundaries,
ignore: ['**/node_modules/**'].concat(packageJson['hot-hook']?.ignore || []),
})
2 changes: 1 addition & 1 deletion packages/hot_hook/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface InitOptions {
/**
* Path to the root file of the application.
*/
root: string
root?: string

/**
* Files that will create an HMR boundary. This is equivalent of importing
Expand Down
2 changes: 0 additions & 2 deletions packages/hot_hook/tests/loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,6 @@ test.group('Loader', () => {
if (import.meta.hot) {
process.send({ type: 'ok' })
}
console.log(import.meta.hot)
console.log("Hello")
`
)
await fs.create(
Expand Down
Loading

0 comments on commit fa6ed2e

Please sign in to comment.