@roots/bud-server
Version:
Development server for @roots/bud
135 lines (107 loc) • 3.42 kB
text/typescript
/* eslint-disable no-console */
import type {Bud} from '@roots/bud-framework'
import type {
MultiCompiler,
StatsCompilation,
StatsModule,
} from '@roots/bud-framework/config'
import type {MiddlewareFactory} from '@roots/bud-server/middleware'
import type {Payload} from '@roots/bud-server/middleware/hot'
import type {RequestHandler} from '@roots/bud-support/express'
import type {Handler} from 'express-serve-static-core'
import {HotEventStream} from '@roots/bud-server/middleware/hot'
import loggerInstance from '@roots/bud-support/logger'
const middlewarePath = `/bud/hot`
let latestStats: null | StatsCompilation = null
let closed = false
let logger: typeof loggerInstance
export const factory: MiddlewareFactory = (app: Bud) => {
if (!app.compiler) return
logger = loggerInstance.scope(app.label, `hmr`) as typeof logger
return makeHandler(app.compiler.instance)
}
export const makeHandler = (compiler: MultiCompiler): Handler => {
const stream = new HotEventStream()
const onInvalid = () => {
if (closed) return
stream.publish({action: `building`})
}
const onDone = (stats: StatsCompilation) => {
if (closed) return
latestStats = stats
publish(`built`, latestStats, stream)
}
compiler.hooks.invalid.tap(`bud-hot-middleware`, onInvalid)
compiler.hooks.done.tap(`bud-hot-middleware`, onDone)
const middleware: RequestHandler = function (req, res, next) {
if (closed) return next()
if (!req.url.endsWith(middlewarePath)) return next()
stream.handle(req, res)
if (latestStats) {
publish(`sync`, latestStats, stream)
}
}
// @ts-ignore
middleware.publish = function (payload) {
if (closed) return
stream.publish(payload)
}
// @ts-ignore
middleware.close = function () {
if (closed) return
closed = true
stream.close()
// @ts-ignore https://github.com/webpack/tapable/issues/32#issuecomment-350644466
stream = null
}
return middleware
}
export const publish = (
action: Payload['action'],
statsCompilation: StatsCompilation,
stream: HotEventStream,
) => {
const compilations = collectCompilations(
statsCompilation.toJson({
all: false,
assets: true,
errorDetails: false,
errors: true,
hash: true,
timings: true,
warnings: true,
}),
)
compilations.forEach((stats: StatsCompilation) => {
const name: string = stats.name ?? statsCompilation.name ?? `unnamed`
const modules = collectModules(stats.modules)
logger.log(`built`, name, `(${stats.hash})`, `in`, `${stats.time}ms`)
stream.publish({
action,
errors: stats.errors ?? [],
hash: stats.hash,
modules,
name,
time: stats.time,
warnings: stats.warnings ?? [],
})
})
}
export const collectModules = (modules?: Array<StatsModule>) => {
if (!modules) return {}
return modules?.reduce((modules, module) => {
if (!module.id || !module.name) return modules
return {...modules, [module.id]: module.name}
}, {})
}
export const collectCompilations = (
stats: StatsCompilation,
): Array<StatsCompilation> => {
let collection: Array<StatsCompilation> = []
// Stats has modules, single bundle
if (stats.modules) collection.push(stats)
// Stats has children, multiple bundles
if (stats.children?.length) collection.push(...stats.children)
// Not sure, assume single
return collection
}