Skip to content

Commit 38a2835

Browse files
authored
feat: add bundler and dev server hooks (#75)
* feat: add bundler and devserver hooks * style: lint * refactor: rename register hooks methods
1 parent 51a58cf commit 38a2835

File tree

6 files changed

+173
-4
lines changed

6 files changed

+173
-4
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"quick:test": "cross-env NODE_DEBUG=adonisjs:assembler node --enable-source-maps --loader=ts-node/esm bin/test.ts"
3535
},
3636
"devDependencies": {
37-
"@adonisjs/application": "^8.0.2",
37+
"@adonisjs/application": "8.1.0",
3838
"@adonisjs/eslint-config": "^1.2.1",
3939
"@adonisjs/prettier-config": "^1.2.1",
4040
"@adonisjs/tsconfig": "^1.2.1",
@@ -66,6 +66,8 @@
6666
"@antfu/install-pkg": "^0.3.1",
6767
"@poppinss/chokidar-ts": "^4.1.3",
6868
"@poppinss/cliui": "^6.3.0",
69+
"@poppinss/hooks": "^7.2.2",
70+
"@poppinss/utils": "^6.7.2",
6971
"cpy": "^11.0.0",
7072
"dedent": "^1.5.1",
7173
"execa": "^8.0.1",

src/bundler.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { join, relative } from 'node:path'
1616
import { cliui, type Logger } from '@poppinss/cliui'
1717
import { detectPackageManager } from '@antfu/install-pkg'
1818

19+
import { AssemblerHooks } from './hooks.js'
1920
import type { BundlerOptions } from './types.js'
2021
import { run, parseConfig, copyFiles } from './helpers.js'
2122

@@ -62,6 +63,7 @@ export class Bundler {
6263
#cwdPath: string
6364
#ts: typeof tsStatic
6465
#logger = ui.logger
66+
#hooks: AssemblerHooks
6567
#options: BundlerOptions
6668

6769
/**
@@ -76,6 +78,7 @@ export class Bundler {
7678
this.#cwdPath = fileURLToPath(this.#cwd)
7779
this.#ts = ts
7880
this.#options = options
81+
this.#hooks = new AssemblerHooks(options.hooks)
7982
}
8083

8184
/**
@@ -197,6 +200,8 @@ export class Bundler {
197200
* Bundles the application to be run in production
198201
*/
199202
async bundle(stopOnError: boolean = true, client?: SupportedPackageManager): Promise<boolean> {
203+
await this.#hooks.registerBuildHooks()
204+
200205
/**
201206
* Step 1: Parse config file to get the build output directory
202207
*/
@@ -220,7 +225,12 @@ export class Bundler {
220225
}
221226

222227
/**
223-
* Step 4: Build typescript source code
228+
* Step 4: Execute build starting hook
229+
*/
230+
await this.#hooks.onBuildStarting({ colors: ui.colors, logger: this.#logger })
231+
232+
/**
233+
* Step 5: Build typescript source code
224234
*/
225235
this.#logger.info('compiling typescript source', { suffix: 'tsc' })
226236
const buildCompleted = await this.#runTsc(outDir)
@@ -251,7 +261,7 @@ export class Bundler {
251261
}
252262

253263
/**
254-
* Step 5: Copy meta files to the build directory
264+
* Step 6: Copy meta files to the build directory
255265
*/
256266
const pkgManager = await this.#getPackageManager(client)
257267
const pkgFiles = pkgManager ? ['package.json', pkgManager.lockFile] : ['package.json']
@@ -261,6 +271,11 @@ export class Bundler {
261271
this.#logger.success('build completed')
262272
this.#logger.log('')
263273

274+
/**
275+
* Step 7: Execute build completed hook
276+
*/
277+
await this.#hooks.onBuildCompleted({ colors: ui.colors, logger: this.#logger })
278+
264279
/**
265280
* Next steps
266281
*/

src/dev_server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { type ExecaChildProcess } from 'execa'
1414
import { cliui, type Logger } from '@poppinss/cliui'
1515
import type { Watcher } from '@poppinss/chokidar-ts'
1616

17+
import { AssemblerHooks } from './hooks.js'
1718
import type { DevServerOptions } from './types.js'
1819
import { AssetsDevServer } from './assets_dev_server.js'
1920
import { getPort, isDotEnvFile, runNode, watch } from './helpers.js'
@@ -84,6 +85,11 @@ export class DevServer {
8485
*/
8586
#assetsServer?: AssetsDevServer
8687

88+
/**
89+
* Hooks to execute custom actions during the dev server lifecycle
90+
*/
91+
#hooks: AssemblerHooks
92+
8793
/**
8894
* Getting reference to colors library from logger
8995
*/
@@ -94,6 +100,7 @@ export class DevServer {
94100
constructor(cwd: URL, options: DevServerOptions) {
95101
this.#cwd = cwd
96102
this.#options = options
103+
this.#hooks = new AssemblerHooks(options.hooks)
97104

98105
this.#isMetaFileWithReloadsEnabled = picomatch(
99106
(this.#options.metaFiles || [])
@@ -147,7 +154,7 @@ export class DevServer {
147154
scriptArgs: this.#options.scriptArgs,
148155
})
149156

150-
this.#httpServer.on('message', (message) => {
157+
this.#httpServer.on('message', async (message) => {
151158
if (this.#isAdonisJSReadyMessage(message)) {
152159
const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host
153160

@@ -167,6 +174,8 @@ export class DevServer {
167174
}
168175

169176
displayMessage.render()
177+
178+
await this.#hooks.onDevServerStarted({ colors: ui.colors, logger: this.#logger })
170179
}
171180
})
172181

@@ -241,6 +250,8 @@ export class DevServer {
241250
* Handles TypeScript source file change
242251
*/
243252
#handleSourceFileChange(action: string, port: string, relativePath: string) {
253+
void this.#hooks.onSourceFileChanged({ colors: ui.colors, logger: this.#logger }, relativePath)
254+
244255
this.#clearScreen()
245256
this.#logger.log(`${this.#colors.green(action)} ${relativePath}`)
246257
this.#restartHTTPServer(port)

src/hooks.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
AssemblerHookNode,
3+
SourceFileChangedHookHandler,
4+
AssemblerHookHandler,
5+
RcFile,
6+
} from '@adonisjs/application/types'
7+
import { RuntimeException } from '@poppinss/utils'
8+
import Hooks from '@poppinss/hooks'
9+
10+
export class AssemblerHooks {
11+
#config: RcFile['unstable_assembler']
12+
13+
#hooks = new Hooks<{
14+
onDevServerStarted: [Parameters<AssemblerHookHandler>, []]
15+
onSourceFileChanged: [Parameters<SourceFileChangedHookHandler>, []]
16+
onBuildStarting: [Parameters<AssemblerHookHandler>, []]
17+
onBuildCompleted: [Parameters<AssemblerHookHandler>, []]
18+
}>()
19+
20+
constructor(config: RcFile['unstable_assembler']) {
21+
this.#config = config
22+
}
23+
24+
/**
25+
* Resolve the hook by importing the file and returning the default export
26+
*/
27+
async #resolveHookNode(node: AssemblerHookNode<any>) {
28+
const exports = await node()
29+
30+
if (!exports.default) {
31+
throw new RuntimeException('Assembler hook must be defined using the default export')
32+
}
33+
34+
return exports.default
35+
}
36+
37+
/**
38+
* Resolve hooks needed for dev-time and register them to the Hooks instance
39+
*/
40+
async registerDevServerHooks() {
41+
await Promise.all([
42+
this.#config?.onDevServerStarted?.map(async (node) =>
43+
this.#hooks.add('onDevServerStarted', await this.#resolveHookNode(node))
44+
),
45+
this.#config?.onSourceFileChanged?.map(async (node) =>
46+
this.#hooks.add('onSourceFileChanged', await this.#resolveHookNode(node))
47+
),
48+
])
49+
}
50+
51+
/**
52+
* Resolve hooks needed for build-time and register them to the Hooks instance
53+
*/
54+
async registerBuildHooks() {
55+
await Promise.all([
56+
this.#config?.onBuildStarting?.map(async (node) =>
57+
this.#hooks.add('onBuildStarting', await this.#resolveHookNode(node))
58+
),
59+
this.#config?.onBuildCompleted?.map(async (node) =>
60+
this.#hooks.add('onBuildCompleted', await this.#resolveHookNode(node))
61+
),
62+
])
63+
}
64+
65+
/**
66+
* When the dev server is started
67+
*/
68+
async onDevServerStarted(...args: Parameters<AssemblerHookHandler>) {
69+
await this.#hooks.runner('onDevServerStarted').run(...args)
70+
}
71+
72+
/**
73+
* When a source file changes
74+
*/
75+
async onSourceFileChanged(...args: Parameters<SourceFileChangedHookHandler>) {
76+
await this.#hooks.runner('onSourceFileChanged').run(...args)
77+
}
78+
79+
/**
80+
* When the build process is starting
81+
*/
82+
async onBuildStarting(...args: Parameters<AssemblerHookHandler>) {
83+
await this.#hooks.runner('onBuildStarting').run(...args)
84+
}
85+
86+
/**
87+
* When the build process is completed
88+
*/
89+
async onBuildCompleted(...args: Parameters<AssemblerHookHandler>) {
90+
await this.#hooks.runner('onBuildCompleted').run(...args)
91+
}
92+
}

src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* file that was distributed with this source code.
88
*/
99

10+
import type { RcFile } from '@adonisjs/application/types'
11+
1012
/**
1113
* Options needed to run a script file
1214
*/
@@ -114,6 +116,13 @@ export type DevServerOptions = {
114116
* Assets bundler options to start its dev server
115117
*/
116118
assets?: AssetsBundlerOptions
119+
/**
120+
* Hooks to execute at different stages
121+
*/
122+
hooks?: Pick<
123+
NonNullable<RcFile['unstable_assembler']>,
124+
'onDevServerStarted' | 'onSourceFileChanged'
125+
>
117126
}
118127

119128
/**
@@ -206,6 +215,11 @@ export type BundlerOptions = {
206215
* for assets
207216
*/
208217
assets?: AssetsBundlerOptions
218+
219+
/**
220+
* Hooks to execute at different stages
221+
*/
222+
hooks?: Pick<NonNullable<RcFile['unstable_assembler']>, 'onBuildCompleted' | 'onBuildStarting'>
209223
}
210224

211225
/**

tests/bundler.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,39 @@ test.group('Bundler', () => {
154154
const aceFile = await fs.contents('./build/ace.js')
155155
assert.notInclude(aceFile, 'ts-node')
156156
})
157+
158+
test('execute hooks', async ({ assert, fs }) => {
159+
assert.plan(2)
160+
161+
await Promise.all([
162+
fs.create(
163+
'tsconfig.json',
164+
JSON.stringify({ compilerOptions: { outDir: 'build', skipLibCheck: true } })
165+
),
166+
fs.create('adonisrc.ts', 'export default { hooks: { onBuildStarting: [() => {}] } }'),
167+
fs.create('package.json', '{}'),
168+
fs.create('package-lock.json', '{}'),
169+
])
170+
171+
const bundler = new Bundler(fs.baseUrl, ts, {
172+
hooks: {
173+
onBuildStarting: [
174+
async () => ({
175+
default: () => {
176+
assert.isTrue(true)
177+
},
178+
}),
179+
],
180+
onBuildCompleted: [
181+
async () => ({
182+
default: () => {
183+
assert.isTrue(true)
184+
},
185+
}),
186+
],
187+
},
188+
})
189+
190+
await bundler.bundle()
191+
})
157192
})

0 commit comments

Comments
 (0)