Skip to content

Commit 4392202

Browse files
authored
Merge pull request #77 from adonisjs/feat/hmr
feat: hot-hook integration
2 parents e53086a + 57781e4 commit 4392202

File tree

4 files changed

+121
-8
lines changed

4 files changed

+121
-8
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"del-cli": "^5.1.0",
5454
"eslint": "^8.57.0",
5555
"github-label-sync": "^2.3.1",
56+
"hot-hook": "^0.1.9",
5657
"husky": "^9.0.11",
5758
"np": "^10.0.2",
5859
"p-event": "^6.0.1",

src/dev_server.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import picomatch from 'picomatch'
11+
import { relative } from 'node:path'
1112
import type tsStatic from 'typescript'
1213
import prettyHrtime from 'pretty-hrtime'
1314
import { type ExecaChildProcess } from 'execa'
@@ -101,6 +102,9 @@ export class DevServer {
101102
this.#cwd = cwd
102103
this.#options = options
103104
this.#hooks = new AssemblerHooks(options.hooks)
105+
if (this.#options.hmr) {
106+
this.#options.nodeArgs = this.#options.nodeArgs.concat(['--import=hot-hook/register'])
107+
}
104108

105109
this.#isMetaFileWithReloadsEnabled = picomatch(
106110
(this.#options.metaFiles || [])
@@ -134,6 +138,23 @@ export class DevServer {
134138
)
135139
}
136140

141+
/**
142+
* Inspect if child process message is coming from Hot Hook
143+
*/
144+
#isHotHookMessage(message: unknown): message is {
145+
type: string
146+
path: string
147+
paths?: string[]
148+
} {
149+
return (
150+
message !== null &&
151+
typeof message === 'object' &&
152+
'type' in message &&
153+
typeof message.type === 'string' &&
154+
message.type.startsWith('hot-hook:')
155+
)
156+
}
157+
137158
/**
138159
* Conditionally clear the terminal screen
139160
*/
@@ -147,6 +168,7 @@ export class DevServer {
147168
* Starts the HTTP server
148169
*/
149170
#startHTTPServer(port: string, mode: 'blocking' | 'nonblocking') {
171+
const hooksArgs = { colors: ui.colors, logger: this.#logger }
150172
this.#httpServer = runNode(this.#cwd, {
151173
script: this.#scriptFile,
152174
env: { PORT: port, ...this.#options.env },
@@ -155,12 +177,30 @@ export class DevServer {
155177
})
156178

157179
this.#httpServer.on('message', async (message) => {
158-
void this.#hooks.onHttpServerMessage({ colors: ui.colors, logger: this.#logger }, message, {
159-
restartServer: () => {
160-
this.#restartHTTPServer(port)
161-
},
180+
this.#hooks.onHttpServerMessage(hooksArgs, message, {
181+
restartServer: () => this.#restartHTTPServer(port),
162182
})
163183

184+
/**
185+
* Handle Hot-Hook messages
186+
*/
187+
if (this.#isHotHookMessage(message)) {
188+
const path = relative(this.#cwd.pathname, message.path || message.paths?.[0]!)
189+
this.#hooks.onSourceFileChanged(hooksArgs, path)
190+
191+
if (message.type === 'hot-hook:full-reload') {
192+
this.#clearScreen()
193+
this.#logger.log(`${ui.colors.green('full-reload')} ${path}`)
194+
this.#restartHTTPServer(port)
195+
this.#hooks.onDevServerStarted(hooksArgs)
196+
} else if (message.type === 'hot-hook:invalidated') {
197+
this.#logger.log(`${ui.colors.green('invalidated')} ${path}`)
198+
}
199+
}
200+
201+
/**
202+
* Handle AdonisJS ready message
203+
*/
164204
if (this.#isAdonisJSReadyMessage(message)) {
165205
const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host
166206

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ export type AssetsBundlerOptions =
8484
* server
8585
*/
8686
export type DevServerOptions = {
87+
/**
88+
* If the dev server should use HMR
89+
*/
90+
hmr?: boolean
91+
8792
/**
8893
* Arguments to pass to the "bin/server.js" file
8994
* executed a child process

tests/dev_server.spec.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
import ts from 'typescript'
1111
import { test } from '@japa/runner'
12-
import { DevServer } from '../index.js'
12+
import { cliui } from '@poppinss/cliui'
1313
import { setTimeout as sleep } from 'node:timers/promises'
1414

15+
import { DevServer } from '../index.js'
16+
1517
test.group('DevServer', () => {
1618
test('start() execute onDevServerStarted hook', async ({ assert, fs, cleanup }) => {
1719
assert.plan(1)
@@ -110,7 +112,6 @@ test.group('DevServer', () => {
110112
include: ['**/*'],
111113
exclude: [],
112114
})
113-
await fs.create('index.ts', 'console.log("hey")')
114115
await fs.create('bin/server.js', `process.send({ isAdonisJS: true, environment: 'web' })`)
115116
await fs.create('.env', 'PORT=3334')
116117

@@ -145,7 +146,6 @@ test.group('DevServer', () => {
145146
include: ['**/*'],
146147
exclude: [],
147148
})
148-
await fs.create('index.ts', 'console.log("hey")')
149149
await fs.create(
150150
'bin/server.js',
151151
`
@@ -188,7 +188,6 @@ test.group('DevServer', () => {
188188
include: ['**/*'],
189189
exclude: [],
190190
})
191-
await fs.create('index.ts', 'console.log("hey")')
192191
await fs.create(
193192
'bin/server.js',
194193
`
@@ -227,4 +226,72 @@ test.group('DevServer', () => {
227226

228227
assert.deepEqual(receivedMessages.length, 4)
229228
}).timeout(10_000)
229+
230+
test('should restart server if receive hot-hook message', async ({ assert, fs }) => {
231+
await fs.createJson('tsconfig.json', { include: ['**/*'], exclude: [] })
232+
await fs.create(
233+
'bin/server.js',
234+
`process.send({ type: 'hot-hook:full-reload', path: '/foo' });`
235+
)
236+
await fs.create('.env', 'PORT=3334')
237+
238+
const { logger } = cliui({ mode: 'raw' })
239+
const devServer = new DevServer(fs.baseUrl, {
240+
hmr: true,
241+
nodeArgs: [],
242+
scriptArgs: [],
243+
}).setLogger(logger)
244+
245+
await devServer.start()
246+
await sleep(1000)
247+
await devServer.close()
248+
249+
const logMessages = logger.getLogs().map(({ message }) => message)
250+
assert.isAtLeast(logMessages.filter((message) => message.includes('full-reload')).length, 1)
251+
})
252+
253+
test('trigger onDevServerStarted and onSourceFileChanged when hot-hook message is received', async ({
254+
assert,
255+
fs,
256+
}) => {
257+
let onDevServerStartedCalled = false
258+
let onSourceFileChangedCalled = false
259+
260+
await fs.createJson('tsconfig.json', { include: ['**/*'], exclude: [] })
261+
await fs.create(
262+
'bin/server.js',
263+
`process.send({ type: 'hot-hook:full-reload', path: '/foo' });`
264+
)
265+
await fs.create('.env', 'PORT=3334')
266+
267+
const { logger } = cliui({ mode: 'raw' })
268+
const devServer = new DevServer(fs.baseUrl, {
269+
hmr: true,
270+
nodeArgs: [],
271+
scriptArgs: [],
272+
hooks: {
273+
onDevServerStarted: [
274+
async () => ({
275+
default: () => {
276+
onDevServerStartedCalled = true
277+
},
278+
}),
279+
],
280+
onSourceFileChanged: [
281+
async () => ({
282+
default: () => {
283+
onSourceFileChangedCalled = true
284+
},
285+
}),
286+
],
287+
},
288+
}).setLogger(logger)
289+
290+
await devServer.start()
291+
await sleep(1000)
292+
await devServer.close()
293+
294+
assert.isTrue(onDevServerStartedCalled)
295+
assert.isTrue(onSourceFileChangedCalled)
296+
})
230297
})

0 commit comments

Comments
 (0)