@roots/bud-server
Version:
Development server for @roots/bud
268 lines (226 loc) • 6.42 kB
text/typescript
import type {Server as BudServer} from '@roots/bud-framework'
import type {Bud} from '@roots/bud-framework'
import type {
Connection,
Middleware,
Watcher,
} from '@roots/bud-framework/services/server'
import {Service} from '@roots/bud-framework/service'
import {inject} from '@roots/bud-server/inject'
import {bind} from '@roots/bud-support/decorators/bind'
import {BudError} from '@roots/bud-support/errors'
/**
* {@link BudServer}
*/
export class Server extends Service implements BudServer {
/**
* {@link BudServer.application}
*/
public declare application: {set: any; use: any} & Express.Application
/**
* {@link BudServer.availableMiddleware}
*/
public declare availableMiddleware: Record<string, string>
/**
* {@link BudServer.connection}
*/
public declare connection: Connection
/**
* {@link BudServer.watcher}
*/
public declare watcher: Watcher
/**
* {@link BudServer.appliedMiddleware}
*/
public appliedMiddleware: Partial<
Record<keyof Middleware.Available, any>
> = {}
/**
* {@link BudServer.proxyUrl}
* @readonly
*/
public get proxyUrl(): URL {
return this.app.hooks.filter(`dev.proxyUrl`, new URL(`http://0.0.0.0`))
}
/**
* {@link BudServer.publicProxyUrl}
* @readonly
*/
public get publicProxyUrl(): URL {
return this.app.hooks.filter(`dev.publicProxyUrl`, this.proxyUrl)
}
/**
* {@link BudServer.publicUrl}
* @readonly
*/
public get publicUrl(): URL {
return this.app.hooks.filter(`dev.publicUrl`, this.url)
}
/**
* {@link BudServer.url}
* @readonly
*/
public get url(): URL {
const url = this.app.hooks.filter(
`dev.url`,
new URL(`http://0.0.0.0:3000`),
)
if (this.app.context.port) url.port = this.app.context.port
return url
}
/**
* {@link BudServer.applyMiddleware}
*/
public async applyMiddleware() {
await Promise.all(
Object.entries(this.enabledMiddleware).map(
async ([key, signifier]) => {
if (this.app.context.hot === false && key === `hot`) {
this.logger
.scope(this.app.label, `server`, `middleware`, key)
.log(`disabled`, `bud.context.hot is false`)
return
}
/** import middleware */
const {factory} = await this.app.module
.import(signifier, import.meta.url)
.catch(this.catch)
/** save reference to middleware instance */
Object.defineProperty(this.appliedMiddleware, key, {
value: factory(this.app),
})
if (typeof this.appliedMiddleware[key] !== `function`) {
this.logger
.scope(this.app.label, `server`, `middleware`, key)
.log(`unused`)
return
}
this.logger
.scope(this.app.label, `server`, `middleware`, key)
.log(`applied`)
.info(this.appliedMiddleware[key])
/** apply middleware */
this.application.use(this.appliedMiddleware[key])
},
),
).catch(this.catch)
}
/**
* {@link Contract.catch}
*/
public override catch(error: BudError | string): never {
throw error
}
/**
* {@link BudServer.compilerBefore}
*/
public override async compilerBefore?(bud: Bud) {
await this.setConnection()
await this.injectScripts()
}
/**
* {@link BudServer.enabledMiddleware}
* @readonly
*/
public get enabledMiddleware(): BudServer['enabledMiddleware'] {
return this.app.hooks.filter(`dev.middleware.enabled`, [])?.reduce(
(enabled, key) => ({
...enabled,
[key]: this.availableMiddleware?.[key],
}),
{},
)
}
/**
* {@link BudServer.injectScripts}
*/
public async injectScripts() {
if (!this.app.isDevelopment) return
const injectOn = async (instance: Bud) =>
inject(
instance,
Array.from(this.app.hooks.filter(`dev.client.scripts`) ?? []),
)
this.app.hasChildren
? Object.values(this.app.children).map(injectOn)
: injectOn(this.app)
}
/**
* {@link BudServer.register}
*/
public override async register?(bud: Bud) {
if (!bud.isDevelopment) return
this.availableMiddleware = {
cookie: ` /bud-server/middleware/cookie`,
dev: ` /bud-server/middleware/dev`,
hot: ` /bud-server/middleware/hot`,
proxy: ` /bud-server/middleware/proxy`,
}
this.application = await bud.module
.import(` /bud-support/express`, import.meta.url)
.then(app => app())
this.logger.log(
`server.application`,
this.application?.constructor.name,
)
this.watcher = await import(` /bud-server/server/watcher`).then(
({Watcher}) => new Watcher(() => bud),
)
this.logger.log(`server.watcher`, this.watcher?.constructor.name)
bud.hooks.on(
`dev.client.scripts`,
await import(` /bud-server/hooks`)
.catch(this.catch)
.then(result => result?.devClientScripts.callback),
)
this.application.set(`x-powered-by`, false)
}
/**
* {@link BudServer.run}
*/
public async run() {
if (this.app.context.dry === true) return
await this.app.hooks.fire(`server.before`, this.app).catch(this.catch)
await this.connection.createServer(this.application).catch(this.catch)
await this.connection.listen()
await this.app.hooks.fire(`server.after`, this.app).catch(this.catch)
}
/**
* {@link BudServer.serverBefore}
*/
public override async serverBefore?(bud: Bud) {
await this.setConnection()
await this.injectScripts()
await bud.compiler.compile(bud)
await this.applyMiddleware()
await this.watcher.watch()
}
/**
* {@link BudServer.setConnection}
*/
public async setConnection(connection?: Connection) {
if (connection) {
this.connection = connection
return this.connection
}
const isHttps = this.url.protocol === `https:`
this.connection = await this.app.module
.import(
isHttps
? ` /bud-server/server/https`
: ` /bud-server/server/http`,
import.meta.url,
)
.then(({Server}) => new Server(this.app))
.catch(this.catch)
return this.connection
}
}