From 8b8492c951140067e5595d4b2dcf51dafb55cc39 Mon Sep 17 00:00:00 2001 From: Kelly Mears Date: Thu, 15 Jun 2023 15:48:59 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20improve:=20error=20handling=20(#232?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Improves error handling - Improves cache validation ## Type of change **PATCH: backwards compatible change** --- .../bud-compiler/src/compiler.service.tsx | 122 +++++++++--------- .../@roots/bud-dashboard/src/app/error.tsx | 6 +- .../dashboard/messages/messages.component.tsx | 8 +- sources/@roots/bud-dashboard/src/service.tsx | 10 +- .../webpack-lifecycle-plugin/index.ts | 2 +- .../src/extension/decorators/index.ts | 21 ++- .../bud-framework/src/extension/index.ts | 30 +++-- .../bud-framework/src/lifecycle/bootstrap.ts | 5 +- sources/@roots/bud-framework/src/module.ts | 21 +-- .../bud-framework/src/types/config/index.ts | 1 + .../@roots/bud-framework/test/module.test.ts | 4 - sources/@roots/bud-hooks/src/event/event.ts | 18 +-- sources/@roots/bud-hooks/src/index.ts | 8 +- sources/@roots/bud-support/src/axios/index.ts | 1 - .../@roots/bud-support/src/decorators/bind.ts | 3 +- .../@roots/bud-support/src/errors/errors.ts | 37 ++---- sources/@roots/bud-support/src/index.ts | 1 - sources/@roots/bud-support/src/ink/index.ts | 2 +- .../@roots/bud-support/src/utilities/files.ts | 71 +++++----- .../@roots/bud-support/src/webpack/index.ts | 42 +++--- sources/@roots/bud/src/cli/app.tsx | 7 +- sources/@roots/bud/src/cli/commands/bud.tsx | 2 +- .../bud/src/cli/commands/bud.upgrade.tsx | 53 ++++---- .../bud/src/cli/commands/doctor/index.tsx | 10 +- sources/@roots/bud/src/services/project.ts | 6 +- 25 files changed, 228 insertions(+), 263 deletions(-) diff --git a/sources/@roots/bud-compiler/src/compiler.service.tsx b/sources/@roots/bud-compiler/src/compiler.service.tsx index 58f8bac77f..4e6c0c332a 100644 --- a/sources/@roots/bud-compiler/src/compiler.service.tsx +++ b/sources/@roots/bud-compiler/src/compiler.service.tsx @@ -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 @@ -50,41 +50,56 @@ export class Compiler extends Service implements Contract.Service { */ @bind public async compile(): Promise { - 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 } @@ -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( + , + ) - try { - Ink.render( - , - ) - } 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, + }) } /** @@ -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 diff --git a/sources/@roots/bud-dashboard/src/app/error.tsx b/sources/@roots/bud-dashboard/src/app/error.tsx index ef995eeb62..48ba19c3ff 100644 --- a/sources/@roots/bud-dashboard/src/app/error.tsx +++ b/sources/@roots/bud-dashboard/src/app/error.tsx @@ -40,11 +40,11 @@ export const Error = ({error, ...props}: Props) => { {` `} - {error.name} + {error.name ?? `Error`} {` `} - {error.message ? ( + {error.message && ( {figures.cross} @@ -52,7 +52,7 @@ export const Error = ({error, ...props}: Props) => { {error.message} - ) : null} + )} {error.details && ( diff --git a/sources/@roots/bud-dashboard/src/dashboard/messages/messages.component.tsx b/sources/@roots/bud-dashboard/src/dashboard/messages/messages.component.tsx index 7b7522aa64..c2ac5020df 100644 --- a/sources/@roots/bud-dashboard/src/dashboard/messages/messages.component.tsx +++ b/sources/@roots/bud-dashboard/src/dashboard/messages/messages.component.tsx @@ -40,13 +40,11 @@ const Message = ({color, error, figure, type}) => borderStyle="bold" borderTop={false} flexDirection="column" + paddingBottom={1} paddingLeft={1} + paddingTop={1} > - - {`\n`} - {error.message.trim()} - {`\n`} - + {error.message.trim()} ) diff --git a/sources/@roots/bud-dashboard/src/service.tsx b/sources/@roots/bud-dashboard/src/service.tsx index 844368e6d9..1cece628ca 100644 --- a/sources/@roots/bud-dashboard/src/service.tsx +++ b/sources/@roots/bud-dashboard/src/service.tsx @@ -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, @@ -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 && ( diff --git a/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts b/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts index dcb4e3ed51..dc4d51a107 100644 --- a/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts @@ -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.`, diff --git a/sources/@roots/bud-framework/src/extension/decorators/index.ts b/sources/@roots/bud-framework/src/extension/decorators/index.ts index 332094c7c0..b871653f1c 100644 --- a/sources/@roots/bud-framework/src/extension/decorators/index.ts +++ b/sources/@roots/bud-framework/src/extension/decorators/index.ts @@ -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' diff --git a/sources/@roots/bud-framework/src/extension/index.ts b/sources/@roots/bud-framework/src/extension/index.ts index db8ff34369..1dfbf66a56 100644 --- a/sources/@roots/bud-framework/src/extension/index.ts +++ b/sources/@roots/bud-framework/src/extension/index.ts @@ -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' @@ -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> = { [K in keyof T as `${K & string}`]?: T[K] @@ -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`) + }) } /** @@ -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, }, }) @@ -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, }, }) diff --git a/sources/@roots/bud-framework/src/lifecycle/bootstrap.ts b/sources/@roots/bud-framework/src/lifecycle/bootstrap.ts index 5794fbe7ae..de5da77d97 100644 --- a/sources/@roots/bud-framework/src/lifecycle/bootstrap.ts +++ b/sources/@roots/bud-framework/src/lifecycle/bootstrap.ts @@ -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] diff --git a/sources/@roots/bud-framework/src/module.ts b/sources/@roots/bud-framework/src/module.ts index b1e56073f8..e87c05797b 100644 --- a/sources/@roots/bud-framework/src/module.ts +++ b/sources/@roots/bud-framework/src/module.ts @@ -1,12 +1,11 @@ import {bind} from '@roots/bud-support/decorators/bind' -import {ImportError} from '@roots/bud-support/errors' +import {ModuleError} from '@roots/bud-support/errors' import {resolve} from '@roots/bud-support/import-meta-resolve' import get from '@roots/bud-support/lodash/get' import set from '@roots/bud-support/lodash/set' import args from '@roots/bud-support/utilities/args' import logger from '@roots/bud-support/utilities/logger' import {paths} from '@roots/bud-support/utilities/paths' -import {createRequire} from 'node:module' import {join, normalize, relative} from 'node:path' import {fileURLToPath, pathToFileURL} from 'node:url' @@ -22,11 +21,6 @@ export class Module extends Service { */ public cacheValid: boolean - /** - * Node require - */ - public require: NodeRequire - /** * Resolved module cache */ @@ -37,11 +31,10 @@ export class Module extends Service { */ @bind public override async bootstrap(bud: Bud) { - this.require = createRequire(this.makeContextURL(bud.context.basedir)) - if (this.cacheEnabled && (await bud.fs.exists(this.cacheLocation))) { try { const data = await bud.fs.read(this.cacheLocation) + logger .scope(`module`) .info(`cache is enabled and cached resolutions exist`) @@ -103,7 +96,7 @@ export class Module extends Service { */ @bind public async import(signifier: T, context?: string) { - if (signifier in this.resolved) { + if (this.resolved && signifier in this.resolved) { const m = await import(get(this.resolved, [signifier])) return m?.default ?? m } @@ -114,7 +107,7 @@ export class Module extends Service { logger.scope(`module`).info(`imported`, signifier) return result?.default ?? result } catch (error) { - throw new ImportError(`could not import ${signifier}`, { + throw new ModuleError(`could not import ${signifier}`, { props: { details: `Could not import ${signifier}`, origin: error, @@ -157,12 +150,12 @@ export class Module extends Service { ): Promise { let errors = [] - if (signifier in this.resolved) { + if (this.resolved && signifier in this.resolved) { logger .scope(`module`) .info(`[cache hit] ${signifier} => ${this.resolved[signifier]}`) - return get(this.resolved, [signifier]) + return this.resolved[signifier] } logger.scope(`module`).info(`resolving`, signifier) @@ -200,7 +193,7 @@ export class Module extends Service { errors.push(`Could not resolve ${signifier} from ${context}`) - throw new ImportError(`could not resolve ${signifier}`, { + throw new ModuleError(`could not resolve ${signifier}`, { cause: errors.reverse().join(`\n`), }) } diff --git a/sources/@roots/bud-framework/src/types/config/index.ts b/sources/@roots/bud-framework/src/types/config/index.ts index 49b867f218..78975ec1b7 100644 --- a/sources/@roots/bud-framework/src/types/config/index.ts +++ b/sources/@roots/bud-framework/src/types/config/index.ts @@ -20,5 +20,6 @@ export type { StatsError, StatsModule, StatsOptions, + WebpackError, WebpackPluginInstance, } from '@roots/bud-support/webpack' diff --git a/sources/@roots/bud-framework/test/module.test.ts b/sources/@roots/bud-framework/test/module.test.ts index d2fee42d4f..0299be958a 100644 --- a/sources/@roots/bud-framework/test/module.test.ts +++ b/sources/@roots/bud-framework/test/module.test.ts @@ -18,10 +18,6 @@ describe(`@roots/bud-framework`, () => { expect(instance).toBeInstanceOf(Module) }) - it(`should have a require fn`, () => { - expect(instance.require).toEqual(expect.any(Function)) - }) - it(`should have resolve fn`, () => { expect(instance.resolve).toEqual(expect.any(Function)) }) diff --git a/sources/@roots/bud-hooks/src/event/event.ts b/sources/@roots/bud-hooks/src/event/event.ts index a648d8239b..a7684515eb 100644 --- a/sources/@roots/bud-hooks/src/event/event.ts +++ b/sources/@roots/bud-hooks/src/event/event.ts @@ -27,25 +27,17 @@ export class EventHooks extends Hooks { await Promise.all( actions.map(async (action, iteration) => { - this.app.hooks.logger.await( - `executing ${id} callback (${iteration + 1}/${actions.length})`, - ) + const timeKey = `${id} (${iteration + 1}/${actions.length})` + + this.app.hooks.logger.time(timeKey) await action(value as any) .catch(error => { - this.app.hooks.logger.error( - `error executing ${id} callback (${iteration + 1}/${ - actions.length - })`, - ) + this.app.hooks.logger.timeEnd(timeKey) throw BudError.normalize(error) }) .then(() => { - logger.success( - `executed ${id} callback (${iteration + 1}/${ - actions.length - })`, - ) + logger.timeEnd(timeKey) }) }), ) diff --git a/sources/@roots/bud-hooks/src/index.ts b/sources/@roots/bud-hooks/src/index.ts index 18a63cc23f..35534a5856 100644 --- a/sources/@roots/bud-hooks/src/index.ts +++ b/sources/@roots/bud-hooks/src/index.ts @@ -2,14 +2,10 @@ // Licensed under the MIT license. /** - * Hooks system used for framework eventing. + * bud.js hooks * * @see https://bud.js.org * @see https://github.com/roots/bud - * - * @packageDocumentation */ -import {Hooks} from './service.js' - -export default Hooks +export {Hooks as default} from './service.js' diff --git a/sources/@roots/bud-support/src/axios/index.ts b/sources/@roots/bud-support/src/axios/index.ts index d387331713..cc068d3822 100644 --- a/sources/@roots/bud-support/src/axios/index.ts +++ b/sources/@roots/bud-support/src/axios/index.ts @@ -1,2 +1 @@ export {default} from 'axios' -export * from 'axios' diff --git a/sources/@roots/bud-support/src/decorators/bind.ts b/sources/@roots/bud-support/src/decorators/bind.ts index 86f7f343dc..9fe06dfd44 100644 --- a/sources/@roots/bud-support/src/decorators/bind.ts +++ b/sources/@roots/bud-support/src/decorators/bind.ts @@ -9,7 +9,7 @@ export function bind(target: any, key: any, descriptor: any) { if (typeof fn !== `function`) { throw new Error( - `@autobind decorator can only be applied to methods not: ${typeof fn}`, + `@bind decorator can only be applied to methods not: ${typeof fn}`, ) } // In IE11 calling Object.defineProperty has a side-effect of evaluating the // getter for the property which is being replaced. This causes infinite @@ -30,6 +30,7 @@ export function bind(target: any, key: any, descriptor: any) { } let boundFn = fn.bind(this) + definingProperty = true Object.defineProperty(this, key, { configurable: true, diff --git a/sources/@roots/bud-support/src/errors/errors.ts b/sources/@roots/bud-support/src/errors/errors.ts index 82748879b5..208ddde77d 100644 --- a/sources/@roots/bud-support/src/errors/errors.ts +++ b/sources/@roots/bud-support/src/errors/errors.ts @@ -15,6 +15,7 @@ interface BudErrorProps { instance: string isBudError: true issues: URL + message: string origin: BudHandler thrownBy: string } @@ -30,6 +31,7 @@ class BudHandler extends BudBaseError { path: string sha1: string } + public declare instance: `default` | string public isBudError = true public declare issues: false | URL @@ -48,14 +50,8 @@ class BudHandler extends BudBaseError { this.details = options?.props?.details ?? false this.issues = options?.props?.issues ?? false this.docs = options?.props?.docs ?? false - this.isBudError = true - } - public override get message(): string { - return this.message - .replaceAll(/\n/g, `\n\n`) - .replaceAll(process.env.INIT_CWD as string, `$INIT_CWD`) - .replaceAll(process.env.PROJECT_CWD as string, `$PROJECT_CWD`) + this.isBudError = true } } @@ -63,28 +59,17 @@ const BudError = BudBaseError.subclass(`BudError`, { custom: BudHandler, }) -const ModuleError = BudError.subclass(`ModuleError`, { - props: { - details: `Error accessing, writing to, importing or resolving a module.`, - issues: new URL(`https://github.com/roots/bud/issues`), - }, -}) +const CLIError = BudError.subclass(`CLIError`) -const ImportError = ModuleError.subclass(`ImportError`) -const FileReadError = ModuleError.subclass(`FileReadError`, { - props: { - details: `Error reading from a file`, - issues: new URL(`https://github.com/roots/bud/issues`), - }, -}) -const FileWriteError = ModuleError.subclass(`FileWriteError`, { +const ModuleError = BudBaseError.subclass(`ModuleError`, { + custom: BudHandler, props: { - details: `Error writing to a file`, + details: `Error accessing, writing to, importing or resolving a module.`, issues: new URL(`https://github.com/roots/bud/issues`), }, }) -const ConfigError = BudError.subclass(`ConfigError`, { +const ConfigError = BudError.subclass(`ConfigurationError`, { props: { details: `Error processing a project configuration file`, docs: new URL(`https://bud.js.org`), @@ -109,7 +94,7 @@ const ServerError = BudError.subclass(`ServerError`, { docs: new URL(`https://bud.js.org/docs/bud.serve`), }, }) -const ExtensionError = BudError.subclass(`BudErrorError`, { +const ExtensionError = BudError.subclass(`ExtensionError`, { props: { details: `Error in an extension`, docs: new URL(`https://bud.js.org`), @@ -119,12 +104,10 @@ const ExtensionError = BudError.subclass(`BudErrorError`, { export { BudError, BudHandler, + CLIError, CompilerError, ConfigError, ExtensionError, - FileReadError, - FileWriteError, - ImportError, InputError, ModuleError, ServerError, diff --git a/sources/@roots/bud-support/src/index.ts b/sources/@roots/bud-support/src/index.ts index 00886da9a6..a6398d68e4 100644 --- a/sources/@roots/bud-support/src/index.ts +++ b/sources/@roots/bud-support/src/index.ts @@ -9,5 +9,4 @@ * Import from submodules */ -export default {} export {} diff --git a/sources/@roots/bud-support/src/ink/index.ts b/sources/@roots/bud-support/src/ink/index.ts index ee35a52f42..11965db275 100644 --- a/sources/@roots/bud-support/src/ink/index.ts +++ b/sources/@roots/bud-support/src/ink/index.ts @@ -1,8 +1,8 @@ /* eslint-disable n/no-extraneous-import */ export { + type Context, createContext, default, - default as React, forwardRef, memo, useCallback, diff --git a/sources/@roots/bud-support/src/utilities/files.ts b/sources/@roots/bud-support/src/utilities/files.ts index d93e34f372..9c4a6a0c6c 100644 --- a/sources/@roots/bud-support/src/utilities/files.ts +++ b/sources/@roots/bud-support/src/utilities/files.ts @@ -1,25 +1,20 @@ import type * as esbuild from '@roots/bud-support/esbuild' +import type {Filesystem} from '@roots/bud-support/filesystem' +import {BudError, ModuleError} from '@roots/bud-support/errors' +import _get from '@roots/bud-support/lodash/get' +import isEqual from '@roots/bud-support/lodash/isEqual' import omit from '@roots/bud-support/lodash/omit' -import {randomUUID} from 'node:crypto' +import _set from '@roots/bud-support/lodash/set' +import * as filesystem from '@roots/bud-support/utilities/filesystem' +import logger from '@roots/bud-support/utilities/logger' +import {get as getPaths} from '@roots/bud-support/utilities/paths' import {dirname, join, parse} from 'node:path' -import type {Filesystem} from '../filesystem/index.js' - -import {BudError, FileReadError, ImportError} from '../errors/index.js' -import _get from '../lodash/get/index.js' -import isEqual from '../lodash/isEqual/index.js' -import _set from '../lodash/set/index.js' -import * as filesystem from './filesystem.js' -import logger from './logger.js' -import {get as getPaths} from './paths.js' - const DYNAMIC_EXTENSIONS = [`.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, `.mts`] const STATIC_EXTENSIONS = [`.json`, `.json5`, `.yml`, `.yaml`] const COMPATIBLE_EXTENSIONS = [...DYNAMIC_EXTENSIONS, ...STATIC_EXTENSIONS] -const uid = randomUUID() // prevents conflicting fs operations when testing - let transformer: esbuild.transformer let fs: Filesystem let data: Record @@ -154,7 +149,7 @@ async function fetchFileInfo(filename: string) { try { file.module = await fs.read(file.path) } catch (cause) { - handleBudError(FileReadError, cause, file) + handleBudError(cause, file) } } @@ -165,7 +160,7 @@ async function fetchFileInfo(filename: string) { const tmpDir = dirname(file.path) const tmpPath = join( tmpDir, - `${file.sha1}${uid}${file.parsed.ext.replace(`ts`, `js`)}`, + `${file.sha1}${file.parsed.ext.replace(`ts`, `js`)}`, ) const cachePath = join( paths.storage, @@ -187,7 +182,9 @@ async function fetchFileInfo(filename: string) { `file will be cached to ${cachePath}`, ) - await transformConfig({cachePath, file}) + await transformConfig({cachePath, file}).catch(error => { + throw error + }) } logger.scope(`fs`).info(`copying`, cachePath, `to`, tmpPath) @@ -211,18 +208,21 @@ async function fetchFileInfo(filename: string) { return importValue?.default ?? importValue } catch (cause) { await fs.remove(tmpPath) - handleBudError(ImportError, cause, file) + throw cause } } if (file.bud) { - file.module = async () => await getModuleValue() + file.module = async () => + await getModuleValue().catch(error => { + throw BudError.normalize(error) + }) } else { file.module = await getModuleValue() } } catch (cause) { await fs.remove(tmpPath) - handleBudError(ImportError, cause, file) + throw cause } } @@ -245,9 +245,13 @@ async function transformConfig({ logger.time(`compiling ${file.name}`) if (!transformer) { - transformer = await import(`../esbuild/index.js`).then( - async ({getImplementation}) => await getImplementation(file.path), - ) + transformer = await import(`../esbuild/index.js`) + .then( + async ({getImplementation}) => await getImplementation(file.path), + ) + .catch(error => { + throw error + }) } await transformer.build({ @@ -268,22 +272,14 @@ async function transformConfig({ * @param cause - Caught exception * @param file - File that caused the error */ -async function handleBudError( - ErrorClass: typeof FileReadError | typeof ImportError, - cause: unknown, - file: File, -) { +async function handleBudError(cause: unknown, file: File) { if (typeof file.name !== `string`) return if (typeof file.dynamic !== `boolean`) return /** * Construct {@link BudError} object */ - const error = new ErrorClass(file.name, { - props: { - origin: BudError.normalize(cause), - }, - }) + const error = ModuleError.normalize(cause) /* Throw if error occured in a bud config */ if (file.name?.includes(`bud`)) throw error @@ -352,6 +348,17 @@ async function verifyResolutionCache() { await removeResolutions() } } + + await fs.write( + join(paths.storage, `checksum.yml`), + Object.entries(data).reduce( + (acc: Record, [key, value]) => { + acc[key] = value.sha1 + return acc + }, + {}, + ), + ) } /** diff --git a/sources/@roots/bud-support/src/webpack/index.ts b/sources/@roots/bud-support/src/webpack/index.ts index 3ee87a06a6..e8669422bb 100644 --- a/sources/@roots/bud-support/src/webpack/index.ts +++ b/sources/@roots/bud-support/src/webpack/index.ts @@ -1,23 +1,25 @@ export {default} from 'webpack' -export { - type AssetInfo, - type Chunk, - type Compilation, - type Compiler, - type Configuration, - type DefinePlugin, - type HotModuleReplacementPlugin, - type MultiCompiler, - type MultiStats, - type PathData, - type ProvidePlugin, - type RuleSetRule, - type StatsAsset, - type StatsChunkGroup, - type StatsCompilation, - type StatsError, - type StatsModule, - type StatsOptions, - type WebpackPluginInstance, +export type { + AssetInfo, + Chunk, + Compilation, + Compiler, + Configuration, + DefinePlugin, + HotModuleReplacementPlugin, + MultiCompiler, + MultiStats, + PathData, + ProvidePlugin, + RuleSetRule, + StatsAsset, + StatsChunkGroup, + StatsCompilation, + StatsError, + StatsModule, + StatsOptions, + StatsProfile, + WebpackError, + WebpackPluginInstance, } from 'webpack' diff --git a/sources/@roots/bud/src/cli/app.tsx b/sources/@roots/bud/src/cli/app.tsx index b1d0e2268e..5595bf3d85 100644 --- a/sources/@roots/bud/src/cli/app.tsx +++ b/sources/@roots/bud/src/cli/app.tsx @@ -2,6 +2,7 @@ import type {CommandClass} from '@roots/bud-support/clipanion' import {Error} from '@roots/bud-dashboard/app' import {Builtins, Cli} from '@roots/bud-support/clipanion' +import {CLIError} from '@roots/bud-support/errors' import {render} from '@roots/bud-support/ink' import logger from '@roots/bud-support/logger' import * as args from '@roots/bud-support/utilities/args' @@ -62,8 +63,8 @@ try { } if (!isCLIContext(context)) throw `Invalid context` -} catch (err) { - render() +} catch (error) { + render() global.process.exit(1) } @@ -97,7 +98,7 @@ await Commands.get(application, context) application .runExit(args.raw, context) - .catch(err => render()) + .catch(error => render()) export {application, Builtins, Cli} export type {CommandClass} diff --git a/sources/@roots/bud/src/cli/commands/bud.tsx b/sources/@roots/bud/src/cli/commands/bud.tsx index a7a399758d..c7e927ddce 100644 --- a/sources/@roots/bud/src/cli/commands/bud.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.tsx @@ -312,7 +312,7 @@ export default class BudCommand extends Command { /** * Handle errors */ - public override async catch(error: BudHandler) { + public override async catch(error: BudHandler): Promise { global.process.exitCode = 1 if (!error.isBudError) error = BudError.normalize(error) diff --git a/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx b/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx index 3db117d967..54172c15c5 100644 --- a/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx @@ -43,7 +43,9 @@ export default class BudUpgradeCommand extends BudCommand { ], }) - public pacman?: `npm` | `yarn` + public command: `add` | `install` + + public pacman: `npm` | `yarn` /** * Use an alternative registry @@ -59,17 +61,18 @@ export default class BudUpgradeCommand extends BudCommand { public override async execute() { await this.makeBud() + const pacman = await whichPm(this.bud.context.basedir) if (!isString(pacman) || ![`npm`, `yarn`].includes(pacman)) { throw new BudError(`bud upgrade only supports yarn classic and npm.`) } - this.pacman = pacman as `npm` | `yarn` - const command = this.pacman === `npm` ? `install` : `add` + this.pacman = pacman as `npm` | `yarn` + this.command = this.pacman === `npm` ? `install` : `add` if (!this.version) { const get = await import(`@roots/bud-support/axios`) - .then(({default: axios}) => axios.get) + .then(m => m.default.get) .catch(error => { throw BudError.normalize(error) }) @@ -78,29 +81,23 @@ export default class BudUpgradeCommand extends BudCommand { `https://registry.npmjs.org/@roots/bud/latest`, ) .then(async res => res.data?.version) - .catch(error => { - throw BudError.normalize(error) - }) + .catch(this.catch) } if (this.hasUpgradeableDependencies(`devDependencies`)) { - try { - await this.$(this.pacman, [ - command, - ...this.getUpgradeableDependencies(`devDependencies`), - ...this.getFlags(`devDependencies`), - ]) - } catch (error) { - throw BudError.normalize(error) - } + await this.$(this.pacman, [ + this.command, + ...this.getUpgradeableDependencies(`devDependencies`), + ...this.getFlags(`devDependencies`), + ]).catch(this.catch) } if (this.hasUpgradeableDependencies(`dependencies`)) { await this.$(this.pacman, [ - command, + this.command, ...this.getUpgradeableDependencies(`dependencies`), ...this.getFlags(`dependencies`), - ]) + ]).catch(this.catch) } } @@ -108,16 +105,17 @@ export default class BudUpgradeCommand extends BudCommand { public getAllDependenciesOfType( type: `dependencies` | `devDependencies`, ): Array { - if (this.bud?.context.manifest?.[type]) { - return Object.keys(this.bud.context.manifest[type]) - } - return [] + return this.bud?.context.manifest?.[type] + ? Object.keys(this.bud.context.manifest[type]) + : [] } @bind public getFlags(type: `dependencies` | `devDependencies`) { const flags = [] + if (this.registry) flags.push(`--registry`, this.registry) + if (type === `devDependencies`) { switch (this.pacman) { case `npm`: @@ -133,8 +131,6 @@ export default class BudUpgradeCommand extends BudCommand { flags.push(`--save`) } - if (this.registry) flags.push(`--registry`, this.registry) - return flags } @@ -142,14 +138,9 @@ export default class BudUpgradeCommand extends BudCommand { public getUpgradeableDependencies( type: `dependencies` | `devDependencies`, ): Array { - const onlyBud = (pkg: string) => - pkg.startsWith(`@roots/`) || pkg.includes(`bud-`) - - const toScope = (pkg: string) => `${pkg}@${this.version}` - return this.getAllDependenciesOfType(type) - .filter(onlyBud) - .map(toScope) + .filter(pkg => pkg.startsWith(`@roots/`) || pkg.includes(`bud-`)) + .map(pkg => `${pkg}@${this.version}`) .filter(Boolean) } diff --git a/sources/@roots/bud/src/cli/commands/doctor/index.tsx b/sources/@roots/bud/src/cli/commands/doctor/index.tsx index 9c4d8de203..3404fc7541 100644 --- a/sources/@roots/bud/src/cli/commands/doctor/index.tsx +++ b/sources/@roots/bud/src/cli/commands/doctor/index.tsx @@ -265,16 +265,16 @@ for a lot of edge cases so it might return a false positive. Environment{`\n`} {this.bud.env.getEntries().map(([key, value]) => { + const color = value.length === 0 ? `yellow` : `dimColor` return ( {figures.triangleRightSmall} {` `} - {key} + {key} {` `} - - {typeof value === `string` && value.length > 0 - ? `************` - : typeof value} + + + {value.length > 0 ? `************` : `empty string`} ) diff --git a/sources/@roots/bud/src/services/project.ts b/sources/@roots/bud/src/services/project.ts index d0da3fd550..651b0bf36d 100644 --- a/sources/@roots/bud/src/services/project.ts +++ b/sources/@roots/bud/src/services/project.ts @@ -2,7 +2,7 @@ import type {Bud} from '@roots/bud' import {Service} from '@roots/bud-framework/service' import {bind} from '@roots/bud-support/decorators/bind' -import {BudError, FileWriteError} from '@roots/bud-support/errors' +import {BudError} from '@roots/bud-support/errors' import omit from '@roots/bud-support/lodash/omit' import * as args from '@roots/bud-support/utilities/args' @@ -40,7 +40,7 @@ export default class Project extends Service { bud.success(`profile written to `, path) } catch (error) { - throw new FileWriteError(`profile.yml`, { + throw new BudError(`Could not write profile.yml`, { props: { details: `An error occurred while writing \`profile.yml\` to the filesystem.`, origin: BudError.normalize(error), @@ -59,7 +59,7 @@ export default class Project extends Service { bud.success(`webpack.output.yml written to`, path) } catch (error) { - throw new FileWriteError(`webpack.output.yml`, { + throw new BudError(`Could not write webpack.output.yml`, { props: { details: `An error occurred while writing \`webpack.output.yml\` to the filesystem.`, origin: BudError.normalize(error),