Skip to content
Draft
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
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test": "c8 --include=src --include=providers --reporter=json-summary npm run quick:test && istanbul-badges-readme",
"prebuild": "npm run lint && npm run clean",
"build": "pnpm clean && tsc && pnpm copy:templates",
"dev": "tsc --watch",
"release": "release-it",
"version": "npm run build",
"prepublishOnly": "npm run build"
Expand All @@ -56,6 +57,10 @@
"typescript": "^5.4.5"
},
"dependencies": {
"emittery": "^1.2.0",
"globby": "^14.1.0",
"hot-hook": "file:../hot-hook/packages/hot_hook",
"@adonisjs/http-server": "file:../http-server",
"reflect-metadata": "^0.2.2"
},
"peerDependencies": {
Expand All @@ -68,6 +73,9 @@
"decorators",
"routing"
],
"resolutions": {
"@adonisjs/http-server": "file:../http-server"
},
"eslintConfig": {
"extends": "@adonisjs/eslint-config/package"
},
Expand Down
16 changes: 14 additions & 2 deletions providers/girouette_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '../src/constants.js'
import { RouteResource } from '@adonisjs/core/http'
import { GirouetteConfig } from '../src/types.js'
import { Girouette } from '../src/girouette.js'

/**
* Represents a route configuration within the Girouette system
Expand Down Expand Up @@ -82,11 +83,22 @@ export default class GirouetteProvider {
this.#controllersPath = path
}

async register() {
this.app.container.singleton(Girouette, async (resolver) => {
const [router, logger] = await Promise.all([resolver.make('router'), resolver.make('logger')])

return new Girouette(this.app, router, logger, {
controllersPath: this.app.httpControllersPath(),
})
})
}

/**
* Boot the provider when the application is ready
*/
async boot() {
// Provider is booted
const girouette = await this.app.container.make(Girouette)
girouette.load()
}

/**
Expand All @@ -96,7 +108,7 @@ export default class GirouetteProvider {
this.#router = await this.app.container.make('router')
this.#logger = await this.app.container.make('logger')
this.#config = this.app.config.get('girouette')
await this.#scanControllersDirectory(this.#controllersPath)
// await this.#scanControllersDirectory(this.#controllersPath)
}

/**
Expand Down
79 changes: 79 additions & 0 deletions src/autoloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Emittery from 'emittery'
import { globby } from 'globby'
import { hot } from 'hot-hook'

export type AutoloaderOptions = {
path: string

/**
* File suffixes for file matching.
*
* @example ['controller']
*/
suffixes: string[]

/**
* File extensions to match.
*
* @default ['ts', 'tsx', 'js', 'jsx']
*/
extensions?: string[]
}

export type AutoloaderEvents = {
added: { path: string; module: any }
updated: { path: string; module: any }
}

export class Autoloader extends Emittery<AutoloaderEvents> {
options: AutoloaderOptions

cache = new Map<string, boolean>()

constructor(options: AutoloaderOptions) {
super()
this.options = options
}

/**
* List matching files for autoloading.
*/
async discover() {
const matches = await globby(this.options.path, {
absolute: true,
expandDirectories: {
files: this.options.suffixes.map((suffix) => `*_${suffix}`),
extensions: ['ts', 'tsx', 'js', 'jsx'],
},
})

return matches
}

/**
* Starts the autoloader.
* It first discover matching files for autoloading and then hook into hot-hook for HMR support.
*/
async autoload() {
const matches = await this.discover()
await Promise.all(matches.map((path) => this.loadModule(path)))
}

async loadModule(path: string) {
const module = await import(path, import.meta.hot?.boundary)

if (this.cache.has(path)) {
await this.emit('updated', { path: path, module })
} else {
await this.emit('added', { path: path, module })
}

if (import.meta.hot) {
this.cache.set(path, true)

hot.dispose(`file://${path}`, () => {
this.loadModule(path)
})
}
}
}
88 changes: 88 additions & 0 deletions src/girouette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ApplicationService, HttpRouterService } from '@adonisjs/core/types'
import { Autoloader } from './autoloader.js'
import { Logger } from '@adonisjs/core/logger'
import { REFLECT_ROUTES_KEY } from './constants.js'
import { Route } from '@adonisjs/http-server'

export type GirouetteOptions = {
controllersPath: string
}

type ControllerModule = {
controller: FunctionConstructor
path: string
}

export class Girouette {
#app: ApplicationService
#router: HttpRouterService
#logger: Logger
#autoloader: Autoloader

cache = new Map<string, Route[]>()

constructor(
app: ApplicationService,
router: HttpRouterService,
logger: Logger,
options: GirouetteOptions
) {
this.#app = app
this.#router = router
this.#logger = logger
this.#autoloader = new Autoloader({
path: options.controllersPath,
suffixes: ['controller'],
})
}

async load() {
this.#autoloader.on('added', ({ module, path }) => {
const { default: Controller } = module
this.updateControllerRoutes(Controller, path)
})

this.#autoloader.on('updated', ({ module, path }) => {
const { default: Controller } = module
this.updateControllerRoutes(Controller, path)
})

await this.#autoloader.autoload()
}

updateControllerRoutes(Controller: FunctionConstructor, path: string) {
const routes = this.#createControllerRoutes(Controller)
this.cache.set(path, routes)
this.reload()
}

reload() {
for (const routes of this.cache.values()) {
for (const route of routes) {
this.#router.pushToRoutes(route)
}
}

this.#router.commit()
}

#createControllerRoutes(Controller: FunctionConstructor) {
const meta = Reflect.getMetadata(REFLECT_ROUTES_KEY, Controller)
if (!meta) return []

const routes = Object.entries(meta).map(([method, route]) =>
this.#createRoute(Controller, method, route)
)

return routes
}

#createRoute(Controller: FunctionConstructor, method: string, route: any) {
return new Route(this.#app, [], {
pattern: route.pattern,
methods: [route.method],
handler: [Controller, method as any],
globalMatchers: {},
})
}
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "@adonisjs/tsconfig/tsconfig.package.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "./build"
"outDir": "./build",
"types": ["hot-hook/import-meta"]
}
}
Loading