Skip to content

Commit

Permalink
feat: add hot-hook/register entrypoint loader
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-R44 committed Apr 9, 2024
1 parent f997613 commit 0a760de
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 1,382 deletions.
4 changes: 0 additions & 4 deletions examples/hono/bin/start.ts

This file was deleted.

8 changes: 7 additions & 1 deletion examples/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"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.tsx",
"dev:tsx": "hot-runner --node-args=\"--import=tsx\" bin/start.ts"
},
"devDependencies": {
Expand All @@ -13,5 +13,11 @@
"dependencies": {
"@hono/node-server": "^1.9.0",
"hono": "^4.1.4"
},
"hot-hook": {
"root": "./bin/server.ts",
"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
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 <[email protected]>",
"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: 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
131 changes: 131 additions & 0 deletions packages/hot_hook/tests/register.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { join } from 'node:path'
import { pEvent } from 'p-event'
import supertest from 'supertest'
import { test } from '@japa/runner'
import { setTimeout } from 'node:timers/promises'

import { createHandlerFile, fakeInstall, runProcess } from './helpers.js'

test.group('Register', () => {
test('Works fine with', async ({ fs }) => {
await fakeInstall(fs.basePath)

await fs.createJson('package.json', { type: 'module' })
await fs.create(
'server.js',
`import * as http from 'http'
import { join } from 'node:path'
const server = http.createServer(async (request, response) => {
const app = await import('./app.js', import.meta.hot?.boundary)
await app.default(request, response)
})
server.listen(3333, () => console.log('Server is running'))`
)

await createHandlerFile({ path: 'app.js', response: 'Hello World!' })

const server = runProcess('server.js', {
cwd: fs.basePath,
env: { NODE_DEBUG: 'hot-hook' },
nodeOptions: ['--import=hot-hook/register'],
})

await server.waitForOutput('Server is running')

await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World!')

await setTimeout(100)
await createHandlerFile({ path: 'app.js', response: 'Hello World! Updated' })
await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World! Updated')

await setTimeout(100)
await createHandlerFile({ path: 'app.js', response: 'Hello World! Updated new' })
await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World! Updated new')
})

test('send full reload message', async ({ fs, assert }) => {
await fakeInstall(fs.basePath)

await fs.createJson('package.json', { type: 'module' })
await fs.create(
'server.js',
`import * as http from 'http'
import { join } from 'node:path'
const server = http.createServer(async (request, response) => {
const app = await import('./app.js')
await app.default(request, response)
})
server.listen(3333, () => console.log('Server is running'))`
)

await createHandlerFile({ path: 'app.js', response: 'Hello World!' })

const server = runProcess('server.js', {
cwd: fs.basePath,
env: { NODE_DEBUG: 'hot-hook' },
nodeOptions: ['--import=hot-hook/register'],
})

await server.waitForOutput('Server is running')

await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World!')
await setTimeout(100)

await createHandlerFile({ path: 'app.js', response: 'Hello World! Updated' })
const result = await pEvent(
server.child,
'message',
(message: any) =>
message?.type === 'hot-hook:full-reload' && message.path === join(fs.basePath, 'app.js')
)
assert.isDefined(result)
})

test('Can define hardcoded boundaries from package json', async ({ fs }) => {
await fakeInstall(fs.basePath)

await fs.createJson('package.json', {
'type': 'module',
'hot-hook': {
boundaries: ['./app.js'],
},
})
await fs.create(
'server.js',
`import * as http from 'http'
import { join } from 'node:path'
const server = http.createServer(async (request, response) => {
const app = await import('./app.js')
await app.default(request, response)
})
server.listen(3333, () => console.log('Server is running'))
`
)

await createHandlerFile({ path: 'app.js', response: 'Hello World!' })

const server = runProcess('server.js', {
cwd: fs.basePath,
env: { NODE_DEBUG: 'hot-hook' },
nodeOptions: ['--import=hot-hook/register'],
})

await server.waitForOutput('Server is running')

await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World!')

await setTimeout(100)
await createHandlerFile({ path: 'app.js', response: 'Hello World! Updated' })
await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World! Updated')

await setTimeout(100)
await createHandlerFile({ path: 'app.js', response: 'Hello World! Updated new' })
await supertest('http://localhost:3333').get('/').expect(200).expect('Hello World! Updated new')
})
})
Loading

0 comments on commit 0a760de

Please sign in to comment.