Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"del-cli": "^5.1.0",
"eslint": "^8.57.0",
"github-label-sync": "^2.3.1",
"hot-hook": "^0.1.9",
"husky": "^9.0.11",
"np": "^10.0.2",
"p-event": "^6.0.1",
Expand Down
48 changes: 44 additions & 4 deletions src/dev_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import picomatch from 'picomatch'
import { relative } from 'node:path'
import type tsStatic from 'typescript'
import prettyHrtime from 'pretty-hrtime'
import { type ExecaChildProcess } from 'execa'
Expand Down Expand Up @@ -101,6 +102,9 @@ export class DevServer {
this.#cwd = cwd
this.#options = options
this.#hooks = new AssemblerHooks(options.hooks)
if (this.#options.hmr) {
this.#options.nodeArgs = this.#options.nodeArgs.concat(['--import=hot-hook/register'])
}

this.#isMetaFileWithReloadsEnabled = picomatch(
(this.#options.metaFiles || [])
Expand Down Expand Up @@ -134,6 +138,23 @@ export class DevServer {
)
}

/**
* Inspect if child process message is coming from Hot Hook
*/
#isHotHookMessage(message: unknown): message is {
type: string
path: string
paths?: string[]
} {
return (
message !== null &&
typeof message === 'object' &&
'type' in message &&
typeof message.type === 'string' &&
message.type.startsWith('hot-hook:')
)
}

/**
* Conditionally clear the terminal screen
*/
Expand All @@ -147,6 +168,7 @@ export class DevServer {
* Starts the HTTP server
*/
#startHTTPServer(port: string, mode: 'blocking' | 'nonblocking') {
const hooksArgs = { colors: ui.colors, logger: this.#logger }
this.#httpServer = runNode(this.#cwd, {
script: this.#scriptFile,
env: { PORT: port, ...this.#options.env },
Expand All @@ -155,12 +177,30 @@ export class DevServer {
})

this.#httpServer.on('message', async (message) => {
void this.#hooks.onHttpServerMessage({ colors: ui.colors, logger: this.#logger }, message, {
restartServer: () => {
this.#restartHTTPServer(port)
},
this.#hooks.onHttpServerMessage(hooksArgs, message, {
restartServer: () => this.#restartHTTPServer(port),
})

/**
* Handle Hot-Hook messages
*/
if (this.#isHotHookMessage(message)) {
const path = relative(this.#cwd.pathname, message.path || message.paths?.[0]!)
this.#hooks.onSourceFileChanged(hooksArgs, path)

if (message.type === 'hot-hook:full-reload') {
this.#clearScreen()
this.#logger.log(`${ui.colors.green('full-reload')} ${path}`)
this.#restartHTTPServer(port)
this.#hooks.onDevServerStarted(hooksArgs)
} else if (message.type === 'hot-hook:invalidated') {
this.#logger.log(`${ui.colors.green('invalidated')} ${path}`)
}
}

/**
* Handle AdonisJS ready message
*/
if (this.#isAdonisJSReadyMessage(message)) {
const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host

Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export type AssetsBundlerOptions =
* server
*/
export type DevServerOptions = {
/**
* If the dev server should use HMR
*/
hmr?: boolean

/**
* Arguments to pass to the "bin/server.js" file
* executed a child process
Expand Down
75 changes: 71 additions & 4 deletions tests/dev_server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

import ts from 'typescript'
import { test } from '@japa/runner'
import { DevServer } from '../index.js'
import { cliui } from '@poppinss/cliui'
import { setTimeout as sleep } from 'node:timers/promises'

import { DevServer } from '../index.js'

test.group('DevServer', () => {
test('start() execute onDevServerStarted hook', async ({ assert, fs, cleanup }) => {
assert.plan(1)
Expand Down Expand Up @@ -110,7 +112,6 @@ test.group('DevServer', () => {
include: ['**/*'],
exclude: [],
})
await fs.create('index.ts', 'console.log("hey")')
await fs.create('bin/server.js', `process.send({ isAdonisJS: true, environment: 'web' })`)
await fs.create('.env', 'PORT=3334')

Expand Down Expand Up @@ -145,7 +146,6 @@ test.group('DevServer', () => {
include: ['**/*'],
exclude: [],
})
await fs.create('index.ts', 'console.log("hey")')
await fs.create(
'bin/server.js',
`
Expand Down Expand Up @@ -188,7 +188,6 @@ test.group('DevServer', () => {
include: ['**/*'],
exclude: [],
})
await fs.create('index.ts', 'console.log("hey")')
await fs.create(
'bin/server.js',
`
Expand Down Expand Up @@ -227,4 +226,72 @@ test.group('DevServer', () => {

assert.deepEqual(receivedMessages.length, 4)
}).timeout(10_000)

test('should restart server if receive hot-hook message', async ({ assert, fs }) => {
await fs.createJson('tsconfig.json', { include: ['**/*'], exclude: [] })
await fs.create(
'bin/server.js',
`process.send({ type: 'hot-hook:full-reload', path: '/foo' });`
)
await fs.create('.env', 'PORT=3334')

const { logger } = cliui({ mode: 'raw' })
const devServer = new DevServer(fs.baseUrl, {
hmr: true,
nodeArgs: [],
scriptArgs: [],
}).setLogger(logger)

await devServer.start()
await sleep(1000)
await devServer.close()

const logMessages = logger.getLogs().map(({ message }) => message)
assert.isAtLeast(logMessages.filter((message) => message.includes('full-reload')).length, 1)
})

test('trigger onDevServerStarted and onSourceFileChanged when hot-hook message is received', async ({
assert,
fs,
}) => {
let onDevServerStartedCalled = false
let onSourceFileChangedCalled = false

await fs.createJson('tsconfig.json', { include: ['**/*'], exclude: [] })
await fs.create(
'bin/server.js',
`process.send({ type: 'hot-hook:full-reload', path: '/foo' });`
)
await fs.create('.env', 'PORT=3334')

const { logger } = cliui({ mode: 'raw' })
const devServer = new DevServer(fs.baseUrl, {
hmr: true,
nodeArgs: [],
scriptArgs: [],
hooks: {
onDevServerStarted: [
async () => ({
default: () => {
onDevServerStartedCalled = true
},
}),
],
onSourceFileChanged: [
async () => ({
default: () => {
onSourceFileChangedCalled = true
},
}),
],
},
}).setLogger(logger)

await devServer.start()
await sleep(1000)
await devServer.close()

assert.isTrue(onDevServerStartedCalled)
assert.isTrue(onSourceFileChangedCalled)
})
})