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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"quick:test": "cross-env NODE_DEBUG=adonisjs:assembler node --enable-source-maps --loader=ts-node/esm bin/test.ts"
},
"devDependencies": {
"@adonisjs/application": "^8.0.2",
"@adonisjs/application": "8.1.0",
"@adonisjs/eslint-config": "^1.2.1",
"@adonisjs/prettier-config": "^1.2.1",
"@adonisjs/tsconfig": "^1.2.1",
Expand Down Expand Up @@ -66,6 +66,8 @@
"@antfu/install-pkg": "^0.3.1",
"@poppinss/chokidar-ts": "^4.1.3",
"@poppinss/cliui": "^6.3.0",
"@poppinss/hooks": "^7.2.2",
"@poppinss/utils": "^6.7.2",
"cpy": "^11.0.0",
"dedent": "^1.5.1",
"execa": "^8.0.1",
Expand Down
19 changes: 17 additions & 2 deletions src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { join, relative } from 'node:path'
import { cliui, type Logger } from '@poppinss/cliui'
import { detectPackageManager } from '@antfu/install-pkg'

import { AssemblerHooks } from './hooks.js'
import type { BundlerOptions } from './types.js'
import { run, parseConfig, copyFiles } from './helpers.js'

Expand Down Expand Up @@ -62,6 +63,7 @@ export class Bundler {
#cwdPath: string
#ts: typeof tsStatic
#logger = ui.logger
#hooks: AssemblerHooks
#options: BundlerOptions

/**
Expand All @@ -76,6 +78,7 @@ export class Bundler {
this.#cwdPath = fileURLToPath(this.#cwd)
this.#ts = ts
this.#options = options
this.#hooks = new AssemblerHooks(options.hooks)
}

/**
Expand Down Expand Up @@ -197,6 +200,8 @@ export class Bundler {
* Bundles the application to be run in production
*/
async bundle(stopOnError: boolean = true, client?: SupportedPackageManager): Promise<boolean> {
await this.#hooks.registerBuildHooks()

/**
* Step 1: Parse config file to get the build output directory
*/
Expand All @@ -220,7 +225,12 @@ export class Bundler {
}

/**
* Step 4: Build typescript source code
* Step 4: Execute build starting hook
*/
await this.#hooks.onBuildStarting({ colors: ui.colors, logger: this.#logger })

/**
* Step 5: Build typescript source code
*/
this.#logger.info('compiling typescript source', { suffix: 'tsc' })
const buildCompleted = await this.#runTsc(outDir)
Expand Down Expand Up @@ -251,7 +261,7 @@ export class Bundler {
}

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

/**
* Step 7: Execute build completed hook
*/
await this.#hooks.onBuildCompleted({ colors: ui.colors, logger: this.#logger })

/**
* Next steps
*/
Expand Down
13 changes: 12 additions & 1 deletion src/dev_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { type ExecaChildProcess } from 'execa'
import { cliui, type Logger } from '@poppinss/cliui'
import type { Watcher } from '@poppinss/chokidar-ts'

import { AssemblerHooks } from './hooks.js'
import type { DevServerOptions } from './types.js'
import { AssetsDevServer } from './assets_dev_server.js'
import { getPort, isDotEnvFile, runNode, watch } from './helpers.js'
Expand Down Expand Up @@ -84,6 +85,11 @@ export class DevServer {
*/
#assetsServer?: AssetsDevServer

/**
* Hooks to execute custom actions during the dev server lifecycle
*/
#hooks: AssemblerHooks

/**
* Getting reference to colors library from logger
*/
Expand All @@ -94,6 +100,7 @@ export class DevServer {
constructor(cwd: URL, options: DevServerOptions) {
this.#cwd = cwd
this.#options = options
this.#hooks = new AssemblerHooks(options.hooks)

this.#isMetaFileWithReloadsEnabled = picomatch(
(this.#options.metaFiles || [])
Expand Down Expand Up @@ -147,7 +154,7 @@ export class DevServer {
scriptArgs: this.#options.scriptArgs,
})

this.#httpServer.on('message', (message) => {
this.#httpServer.on('message', async (message) => {
if (this.#isAdonisJSReadyMessage(message)) {
const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host

Expand All @@ -167,6 +174,8 @@ export class DevServer {
}

displayMessage.render()

await this.#hooks.onDevServerStarted({ colors: ui.colors, logger: this.#logger })
}
})

Expand Down Expand Up @@ -241,6 +250,8 @@ export class DevServer {
* Handles TypeScript source file change
*/
#handleSourceFileChange(action: string, port: string, relativePath: string) {
void this.#hooks.onSourceFileChanged({ colors: ui.colors, logger: this.#logger }, relativePath)

this.#clearScreen()
this.#logger.log(`${this.#colors.green(action)} ${relativePath}`)
this.#restartHTTPServer(port)
Expand Down
92 changes: 92 additions & 0 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
AssemblerHookNode,
SourceFileChangedHookHandler,
AssemblerHookHandler,
RcFile,
} from '@adonisjs/application/types'
import { RuntimeException } from '@poppinss/utils'
import Hooks from '@poppinss/hooks'

export class AssemblerHooks {
#config: RcFile['unstable_assembler']

#hooks = new Hooks<{
onDevServerStarted: [Parameters<AssemblerHookHandler>, []]
onSourceFileChanged: [Parameters<SourceFileChangedHookHandler>, []]
onBuildStarting: [Parameters<AssemblerHookHandler>, []]
onBuildCompleted: [Parameters<AssemblerHookHandler>, []]
}>()

constructor(config: RcFile['unstable_assembler']) {
this.#config = config
}

/**
* Resolve the hook by importing the file and returning the default export
*/
async #resolveHookNode(node: AssemblerHookNode<any>) {
const exports = await node()

if (!exports.default) {
throw new RuntimeException('Assembler hook must be defined using the default export')
}

return exports.default
}

/**
* Resolve hooks needed for dev-time and register them to the Hooks instance
*/
async registerDevServerHooks() {
await Promise.all([
this.#config?.onDevServerStarted?.map(async (node) =>
this.#hooks.add('onDevServerStarted', await this.#resolveHookNode(node))
),
this.#config?.onSourceFileChanged?.map(async (node) =>
this.#hooks.add('onSourceFileChanged', await this.#resolveHookNode(node))
),
])
}

/**
* Resolve hooks needed for build-time and register them to the Hooks instance
*/
async registerBuildHooks() {
await Promise.all([
this.#config?.onBuildStarting?.map(async (node) =>
this.#hooks.add('onBuildStarting', await this.#resolveHookNode(node))
),
this.#config?.onBuildCompleted?.map(async (node) =>
this.#hooks.add('onBuildCompleted', await this.#resolveHookNode(node))
),
])
}

/**
* When the dev server is started
*/
async onDevServerStarted(...args: Parameters<AssemblerHookHandler>) {
await this.#hooks.runner('onDevServerStarted').run(...args)
}

/**
* When a source file changes
*/
async onSourceFileChanged(...args: Parameters<SourceFileChangedHookHandler>) {
await this.#hooks.runner('onSourceFileChanged').run(...args)
}

/**
* When the build process is starting
*/
async onBuildStarting(...args: Parameters<AssemblerHookHandler>) {
await this.#hooks.runner('onBuildStarting').run(...args)
}

/**
* When the build process is completed
*/
async onBuildCompleted(...args: Parameters<AssemblerHookHandler>) {
await this.#hooks.runner('onBuildCompleted').run(...args)
}
}
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* file that was distributed with this source code.
*/

import type { RcFile } from '@adonisjs/application/types'

/**
* Options needed to run a script file
*/
Expand Down Expand Up @@ -114,6 +116,13 @@ export type DevServerOptions = {
* Assets bundler options to start its dev server
*/
assets?: AssetsBundlerOptions
/**
* Hooks to execute at different stages
*/
hooks?: Pick<
NonNullable<RcFile['unstable_assembler']>,
'onDevServerStarted' | 'onSourceFileChanged'
>
}

/**
Expand Down Expand Up @@ -206,6 +215,11 @@ export type BundlerOptions = {
* for assets
*/
assets?: AssetsBundlerOptions

/**
* Hooks to execute at different stages
*/
hooks?: Pick<NonNullable<RcFile['unstable_assembler']>, 'onBuildCompleted' | 'onBuildStarting'>
}

/**
Expand Down
35 changes: 35 additions & 0 deletions tests/bundler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,39 @@ test.group('Bundler', () => {
const aceFile = await fs.contents('./build/ace.js')
assert.notInclude(aceFile, 'ts-node')
})

test('execute hooks', async ({ assert, fs }) => {
assert.plan(2)

await Promise.all([
fs.create(
'tsconfig.json',
JSON.stringify({ compilerOptions: { outDir: 'build', skipLibCheck: true } })
),
fs.create('adonisrc.ts', 'export default { hooks: { onBuildStarting: [() => {}] } }'),
fs.create('package.json', '{}'),
fs.create('package-lock.json', '{}'),
])

const bundler = new Bundler(fs.baseUrl, ts, {
hooks: {
onBuildStarting: [
async () => ({
default: () => {
assert.isTrue(true)
},
}),
],
onBuildCompleted: [
async () => ({
default: () => {
assert.isTrue(true)
},
}),
],
},
})

await bundler.bundle()
})
})