Skip to content

Commit

Permalink
✨ improve: error handling (#2327)
Browse files Browse the repository at this point in the history
- Improves error handling
- Improves cache validation

## Type of change

**PATCH: backwards compatible change**
  • Loading branch information
kellymears committed Jun 15, 2023
1 parent e6938cf commit 8b8492c
Show file tree
Hide file tree
Showing 25 changed files with 228 additions and 263 deletions.
122 changes: 60 additions & 62 deletions sources/@roots/bud-compiler/src/compiler.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import type {
SourceFile,
} from '@roots/bud-support/open'

import * as App from '@roots/bud-dashboard/app'
import {Error} from '@roots/bud-dashboard/app'
import {Service} from '@roots/bud-framework/service'
import {bind} from '@roots/bud-support/decorators/bind'
import {BudError, CompilerError} from '@roots/bud-support/errors'
import {BudError} from '@roots/bud-support/errors'
import {duration} from '@roots/bud-support/human-readable'
import * as Ink from '@roots/bud-support/ink'
import {render} from '@roots/bud-support/ink'
import stripAnsi from '@roots/bud-support/strip-ansi'
import webpack from '@roots/bud-support/webpack'
import {pathToFileURL} from 'node:url'
import webpack from 'webpack'

/**
* Wepback compilation controller class
Expand Down Expand Up @@ -50,41 +50,56 @@ export class Compiler extends Service implements Contract.Service {
*/
@bind
public async compile(): Promise<MultiCompiler> {
const compilerPath = await this.app.module.resolve(
`webpack`,
import.meta.url,
)
this.implementation = await this.app.module.import(
compilerPath,
import.meta.url,
)
this.logger.log(`imported webpack`, this.implementation.version)
const compilerPath = await this.app.module
.resolve(`webpack`, import.meta.url)
.catch(error => {
throw BudError.normalize(error)
})

this.implementation = await this.app.module
.import(compilerPath, import.meta.url)
.catch(error => {
throw BudError.normalize(error)
})
.finally(() => {
this.logger.info(`imported webpack from ${compilerPath}`)
})

this.config = !this.app.hasChildren
? [await this.app.build.make()]
: await Promise.all(
Object.values(this.app.children).map(async (child: Bud) => {
try {
return await child.build.make()
} catch (error) {
throw error
}
}),
Object.values(this.app.children).map(
async (child: Bud) =>
await child.build.make().catch(error => {
throw error
}),
),
)

await this.app.hooks.fire(`compiler.before`, this.app)

this.logger.timeEnd(`initialize`)

this.logger.await(`compilation`)
try {
this.instance = this.implementation(this.config)
} catch (error) {
throw BudError.normalize(error)
}

this.instance = this.implementation(this.config)
this.instance.hooks.done.tap(this.app.label, async (stats: any) => {
await this.onStats(stats)
await this.app.hooks.fire(`compiler.close`, this.app)
})
await this.app.hooks
.fire(`compiler.after`, this.app)
.catch(error => {
this.logger.error(error)
})

await this.app.hooks.fire(`compiler.after`, this.app)
await this.app.hooks
.fire(`compiler.close`, this.app)
.catch(error => {
this.logger.error(error)
})
})

return this.instance
}
Expand All @@ -93,45 +108,30 @@ export class Compiler extends Service implements Contract.Service {
* Compiler error event
*/
@bind
public async onError(error: Error) {
process.exitCode = 1

await this.app.hooks.fire(`compiler.error`, error)
public async onError(error: webpack.WebpackError) {
global.process.exitCode = 1

this.app.isDevelopment &&
this.app.server.appliedMiddleware?.hot?.publish({error})

try {
this.app.notifier.notify({
group: this.app.label,
message: error.message,
subtitle: error.name,
})
} catch (error) {
this.logger.error(error)
}
// @eslint-disable-next-line no-console
render(
<Error
{...new BudError(error.message, {
props: {
error: BudError.normalize(error),
},
})}
/>,
)

try {
Ink.render(
<App.Error
error={
new CompilerError(error.message, {
props: {
details: `This error was thrown by the webpack compiler itself. It is not the same as a syntax error. It is likely a missing or unresolvable build dependency.`,
docs: new URL(`https://bud.js.org/`),
issues: new URL(
`https://github.com/roots/bud/search?q=is:issue+"compiler" in:title`,
),
stack: error.stack,
thrownBy: `webpack`,
},
})
}
/>,
)
} catch (error) {
throw BudError.normalize(error)
}
await this.app.hooks.fire(`compiler.error`, error)

this.app.notifier.notify({
group: this.app.label,
message: error.message,
subtitle: error.name,
})
}

/**
Expand Down Expand Up @@ -226,9 +226,7 @@ export class Compiler extends Service implements Contract.Service {
module?.id === moduleIdent || module?.name === moduleIdent,
)

if (!module) {
return error
}
if (!module) return error

if (module.nameForCondition) {
file = module.nameForCondition
Expand Down
6 changes: 3 additions & 3 deletions sources/@roots/bud-dashboard/src/app/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,19 @@ export const Error = ({error, ...props}: Props) => {
<Box flexDirection="column" paddingTop={1}>
<Text backgroundColor="red" color="white">
{` `}
{error.name}
{error.name ?? `Error`}
{` `}
</Text>

{error.message ? (
{error.message && (
<Box marginTop={1}>
<Text>
<Text color="red">{figures.cross}</Text>
{` `}
{error.message}
</Text>
</Box>
) : null}
)}

{error.details && (
<Box marginTop={1}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ const Message = ({color, error, figure, type}) =>
borderStyle="bold"
borderTop={false}
flexDirection="column"
paddingBottom={1}
paddingLeft={1}
paddingTop={1}
>
<Text>
{`\n`}
{error.message.trim()}
{`\n`}
</Text>
<Text>{error.message.trim()}</Text>
</Box>
</Box>
)
10 changes: 8 additions & 2 deletions sources/@roots/bud-dashboard/src/service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ export class Dashboard extends Service implements Contract {
try {
return (
errors
/* Unhelpful errors passed down the loader chain */
.filter(({message}) => !message?.includes(`HookWebpackError`))
/* Filter unhelpful errors from compiler internals */
.filter(
error => error && !error.message?.includes(`HookWebpackError`),
)
/* Format errors */
.map(({message, ...error}: StatsError) => ({
...error,
Expand Down Expand Up @@ -145,6 +147,10 @@ export class Dashboard extends Service implements Contract {
} catch (error) {}
}

/**
* Render queued messages
*/
@bind
public async renderQueuedMessages() {
render(
this.app.consoleBuffer.queue?.length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export default class BudWebpackLifecyclePlugin extends Extension {
!this.app.hooks.filter(`build.entry`)
) {
this.logger.error(
`\n\nNo entrypoints found.\n\nEither create a file at ${this.app.relPath(
`No entrypoints specified and no module found at @src/index. Either create a file at ${this.app.relPath(
`@src`,
`index.js`,
)} or use the bud.entry method to specify an entrypoint.`,
Expand Down
21 changes: 10 additions & 11 deletions sources/@roots/bud-framework/src/extension/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
export {dependsOn} from '@roots/bud-framework/extension/decorators/dependsOn'
export {dependsOnOptional} from '@roots/bud-framework/extension/decorators/dependsOnOptional'
export {development} from '@roots/bud-framework/extension/decorators/development'
export {disabled} from '@roots/bud-framework/extension/decorators/disabled'
export {expose} from '@roots/bud-framework/extension/decorators/expose'
export {label} from '@roots/bud-framework/extension/decorators/label'
export {options} from '@roots/bud-framework/extension/decorators/options'
export {plugin} from '@roots/bud-framework/extension/decorators/plugin'
export {production} from '@roots/bud-framework/extension/decorators/production'
export {when} from '@roots/bud-framework/extension/decorators/when'
export {bind} from '@roots/bud-support/decorators/bind'
export {deprecated} from '@roots/bud-support/decorators/deprecated'

export {dependsOn} from './dependsOn.js'
export {dependsOnOptional} from './dependsOnOptional.js'
export {development} from './development.js'
export {disabled} from './disabled.js'
export {expose} from './expose.js'
export {label} from './label.js'
export {options} from './options.js'
export {plugin} from './plugin.js'
export {production} from './production.js'
export {when} from './when.js'
30 changes: 16 additions & 14 deletions sources/@roots/bud-framework/src/extension/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {ApplyPluginConstructor} from '@roots/bud-framework/extension/decorators/plugin'

import {bind} from '@roots/bud-support/decorators/bind'
import {BudError, ImportError} from '@roots/bud-support/errors'
import {BudError, ExtensionError} from '@roots/bud-support/errors'
import get from '@roots/bud-support/lodash/get'
import isFunction from '@roots/bud-support/lodash/isFunction'
import isObject from '@roots/bud-support/lodash/isObject'
Expand All @@ -11,7 +13,6 @@ import DynamicOption from '@roots/bud-support/value'
import type {Bud} from '../index.js'
import type {Modules} from '../index.js'
import type {Compiler} from '../types/config/index.js'
import type {ApplyPluginConstructor} from './decorators/plugin.js'

export type Options<T = Record<string, any>> = {
[K in keyof T as `${K & string}`]?: T[K]
Expand Down Expand Up @@ -216,13 +217,15 @@ export class Extension<
if (this.meta[`boot`] === true) return
this.meta[`boot`] = true

try {
await this.boot(this.app)
} catch (error) {
throw error
}

this.logger.success(`booted`)
this.logger.time(`${this.label} boot`)
await this.boot(this.app)
.catch(error => {
this.logger.timeEnd(`${this.label} boot`)
throw ExtensionError.normalize(error)
})
.finally(() => {
this.logger.timeEnd(`${this.label} boot`)
})
}

/**
Expand Down Expand Up @@ -446,9 +449,9 @@ export class Extension<
try {
return await this.app.module.import(signifier, context)
} catch (error) {
throw new ImportError(`could not import ${signifier}`, {
throw new ExtensionError(`could not import ${signifier}`, {
props: {
origin: ImportError.normalize(error),
origin: error,
thrownBy: this.label,
},
})
Expand Down Expand Up @@ -490,10 +493,9 @@ export class Extension<
try {
return await this.app.module.resolve(signifier, context)
} catch (error) {
const cause = BudError.normalize(error)
throw new ImportError(`could not resolve ${signifier}`, {
throw new ExtensionError(`could not resolve ${signifier}`, {
props: {
origin: cause,
origin: error,
thrownBy: this.label,
},
})
Expand Down
5 changes: 3 additions & 2 deletions sources/@roots/bud-framework/src/lifecycle/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ export const bootstrap = async function (this: Bud) {

logger.time(`initialize`)

this[`fs`] = new FS(() => this)
this[`module`] = new Module(() => this)
this.fs = new FS(() => this)
this.module = new Module(() => this)
await this.module.bootstrap(this)

await Promise.all(
[...this.context.services]
Expand Down
Loading

0 comments on commit 8b8492c

Please sign in to comment.