diff --git a/sources/@roots/bud-compiler/src/service/index.tsx b/sources/@roots/bud-compiler/src/service/index.tsx
index dea9114764..6db984b83b 100644
--- a/sources/@roots/bud-compiler/src/service/index.tsx
+++ b/sources/@roots/bud-compiler/src/service/index.tsx
@@ -21,7 +21,6 @@ import {Error as DisplayError} from '@roots/bud-dashboard/components/error'
import {Service} from '@roots/bud-framework/service'
import {bind} from '@roots/bud-support/decorators/bind'
import {BudError} from '@roots/bud-support/errors'
-import {render} from '@roots/bud-support/ink'
import isNull from '@roots/bud-support/lodash/isNull'
import isNumber from '@roots/bud-support/lodash/isNumber'
import isString from '@roots/bud-support/lodash/isString'
@@ -31,11 +30,6 @@ import stripAnsi from '@roots/bud-support/strip-ansi'
* {@link BudCompiler} implementation
*/
class Compiler extends Service implements BudCompiler {
- /**
- * {@link BudCompiler.compilationStats}
- */
- public declare compilationStats: BudCompiler[`compilationStats`]
-
/**
* {@link BudCompiler.config}
*/
@@ -73,9 +67,11 @@ class Compiler extends Service implements BudCompiler {
})
if (`isBudError` in error) {
- render()
+ this.app.context.render()
} else {
- render()
+ this.app.context.render(
+ ,
+ )
}
}
/**
@@ -88,26 +84,21 @@ class Compiler extends Service implements BudCompiler {
? `${this.app.label} (${child.name})`
: child.name
- this.stats = stats
-
- this.compilationStats = stats.toJson(statsOptions)
-
- this.app.dashboard.updateStats(this.compilationStats)
+ this.stats = stats.toJson(statsOptions)
+ this.app.context.render(this.app.dashboard.render(stats))
if (stats.hasErrors()) {
process.exitCode = 1
- this.compilationStats.children = this.compilationStats.children?.map(
- child => ({
- ...child,
- errors:
- child.errors && this.sourceErrors
- ? this.sourceErrors(child.errors)
- : child.errors ?? [],
- }),
- )
+ this.stats.children = this.stats.children?.map(child => ({
+ ...child,
+ errors:
+ child.errors && this.sourceErrors
+ ? this.sourceErrors(child.errors)
+ : child.errors ?? [],
+ }))
- this.compilationStats.children
+ this.stats.children
?.filter(
child => isNumber(child.errorsCount) && child.errorsCount > 0,
)
@@ -131,7 +122,7 @@ class Compiler extends Service implements BudCompiler {
})
}
- this.compilationStats.children
+ this.stats.children
?.filter(child => child.errorsCount === 0)
.forEach(child => {
try {
@@ -146,6 +137,7 @@ class Compiler extends Service implements BudCompiler {
})
this.app.server?.publicUrl.href &&
+ this.app.context.browser &&
this.app.notifier.openBrowser(this.app.server?.publicUrl.href)
} catch (error) {
this.logger.error(error)
@@ -224,8 +216,8 @@ class Compiler extends Service implements BudCompiler {
* In a perfect world webpack plugins would use the
* `nameForCondition` property to identify the module.
*/
- if (ident && this.compilationStats?.children) {
- module = this.compilationStats.children
+ if (ident && this.stats?.children) {
+ module = this.stats.children
.flatMap(child => child?.modules)
.find(module => [module?.id, module?.name].includes(ident))
}
diff --git a/sources/@roots/bud-dashboard/src/application.tsx b/sources/@roots/bud-dashboard/src/application.tsx
index d5da66f752..5f6b316f7a 100644
--- a/sources/@roots/bud-dashboard/src/application.tsx
+++ b/sources/@roots/bud-dashboard/src/application.tsx
@@ -4,6 +4,7 @@ import type {StatsCompilation} from '@roots/bud-framework/config'
import {exit} from 'node:process'
import {Error} from '@roots/bud-dashboard/components/error'
+import Footer from '@roots/bud-dashboard/components/footer'
import Compilation from '@roots/bud-dashboard/views/compilation'
import Debug from '@roots/bud-dashboard/views/debug'
import Server from '@roots/bud-dashboard/views/server'
@@ -16,11 +17,11 @@ import {
useState,
useStdout,
} from '@roots/bud-support/ink'
+import escapes from '@roots/bud-support/ansi-escapes'
export interface Props {
basedir?: string
- close?: (callback: (error?: Error | null) => any) => void
- closed?: boolean
+ close?: (callback: (error?: Error | null) => any) => any
compact?: boolean
compilations?: Array>
debug?: boolean
@@ -32,11 +33,12 @@ export interface Props {
errors?: StatsCompilation[`errors`]
isolated?: number
mode: Bud['mode']
+ notifier?: Bud[`notifier`]
proxy?: boolean
proxyUrl?: URL
publicDevUrl?: URL
publicProxyUrl?: URL
- status?: false | string
+ displayHelp?: boolean
warnings?: StatsCompilation[`warnings`]
}
@@ -56,7 +58,7 @@ export const Application = ({
proxyUrl,
publicDevUrl,
publicProxyUrl,
- status,
+ displayHelp,
}: Props) => {
const {stdout} = useStdout()
@@ -96,6 +98,7 @@ export const Application = ({
return (
<>
{error && }
+
{compilations.map((compilation, id) => (
) => {
- const app = useApp()
+ const onExit = useOnExit()
+ const stdout = useStdout()
+ const [compact, setCompact] = useState(props.compact)
+ const [debug, setDisplayDebug] = useState(props.debug)
+ const [displayAssets, setDisplayAssets] = useState(props.displayAssets)
+ const [displayEntrypoints, setDisplayEntrypoints] = useState(true)
const [displayServerInfo, setDisplayServerInfo] = useState(
props.displayServerInfo,
)
- const [debug, setDisplayDebug] = useState(props.debug)
- const [displayEntrypoints, setDisplayEntrypoints] = useState(true)
- const [displayAssets, setDisplayAssets] = useState(props.displayAssets)
- const [closed, setClosed] = useState(false)
- const [compact, setCompact] = useState(props.compact)
+ const [help, setHelp] = useState(false)
const [isolated, setIsolated] = useState(0)
useInput((key, input) => {
@@ -184,54 +189,80 @@ export const TeletypeApplication = ({
case `a`:
setDisplayAssets(!displayAssets)
break
- case `e`:
- setDisplayEntrypoints(!displayEntrypoints)
+
+ case `b`:
+ if (notifier?.openBrowser && props.devUrl) {
+ notifier.openBrowser(props.devUrl?.toString())
+ notifier.browserOpened = false
+ }
break
+
+ case `c`:
+ setCompact(!compact)
+ break
+
case `d`:
setDisplayDebug(!debug)
break
+
+ case `e`:
+ setDisplayEntrypoints(!displayEntrypoints)
+ break
+
+ case `h`:
+ setHelp(!help)
+ break
+
+ case `q`:
+ onExit()
+ break
+
+ case `r`:
+ stdout.write(escapes.clearTerminal)
+ break
+
case `s`:
setDisplayServerInfo(!displayServerInfo)
break
- case `c`:
- setCompact(!compact)
- break
+
case `0`:
setIsolated(0)
break
- default:
- break
}
new Array(9).fill(0).forEach((_, i) => {
if (!props.compilations) return
+
key === `${i + 1}` &&
isolated !== i + 1 &&
setIsolated(Math.min(i + 1, props.compilations.length))
})
- if (input.escape) {
- setClosed(true)
- if (close)
- close((error?) => {
- if (error) app.exit(error)
- else app.exit()
-
- exit(error ? 1 : 0)
- })
- }
+ if (input.escape) onExit()
})
return (
-
+ <>
+
+
+ >
)
}
+
+const useOnExit = () => {
+ const app = useApp()
+
+ return (error?: Error | null): any => {
+ if (error) app.exit(error)
+ else app.exit()
+ exit(error ? 1 : 0)
+ }
+}
diff --git a/sources/@roots/bud-dashboard/src/components/footer.tsx b/sources/@roots/bud-dashboard/src/components/footer.tsx
new file mode 100644
index 0000000000..914293d361
--- /dev/null
+++ b/sources/@roots/bud-dashboard/src/components/footer.tsx
@@ -0,0 +1,70 @@
+import {Box, Text} from '@roots/bud-support/ink'
+
+export interface Props {
+ display?: boolean
+}
+
+const Footer = ({display}: Props) => {
+ if (!display) {
+ return (
+
+ {` `}
+
+ {` `}Press h{' '}
+ to show help
+
+
+ )
+ }
+
+ return (
+
+ {` `}
+
+ {` `}Shortcuts{` `}
+
+ {` `}
+
+ {` `}
+ Press b to open browser
+
+
+ {` `}
+ Press c{' '}
+ to toggle compact display
+
+
+ {` `}
+ Press d{' '}
+
+ to toggle detailed debug information (may be system intensive)
+
+
+
+ {` `}
+ Press e{' '}
+ to toggle entrypoints display
+
+
+ {` `}
+ Press h to hide help
+
+
+ {` `}
+ Press q to quit
+
+
+ {` `}
+ Press r{' '}
+ to clear/reload console
+
+
+ {` `}
+ Press s{' '}
+ to toggle server info display
+
+
+ )
+}
+
+export default Footer
diff --git a/sources/@roots/bud-dashboard/src/service.tsx b/sources/@roots/bud-dashboard/src/service.tsx
index f164889746..72957443bb 100644
--- a/sources/@roots/bud-dashboard/src/service.tsx
+++ b/sources/@roots/bud-dashboard/src/service.tsx
@@ -5,21 +5,20 @@ import type {
} from '@roots/bud-framework/config'
import type {Dashboard as BudDashboard} from '@roots/bud-framework/services'
-import {stderr, stdin, stdout} from 'node:process'
+import {stdin, stdout} from 'node:process'
import {makeErrorFormatter} from '@roots/bud-dashboard/helpers/formatErrors'
import {Service} from '@roots/bud-framework/service'
import {bind} from '@roots/bud-support/decorators/bind'
-import {Box, type ReactElement, render} from '@roots/bud-support/ink'
+import {Box} from '@roots/bud-support/ink'
import isUndefined from '@roots/bud-support/lodash/isUndefined'
-import patchConsole from '@roots/bud-support/patch-console'
import {Application, TeletypeApplication} from './application.js'
type Compilations = Array>
/**
- * {@link BudDashboard} implementation
+ * {@link BudDashboard}
*/
export class Dashboard extends Service implements BudDashboard {
/**
@@ -29,111 +28,44 @@ export class Dashboard extends Service implements BudDashboard {
errors: StatsError[] | undefined,
) => StatsError[] | undefined
- /**
- * {@link BudDashboard.instance}
- */
- public declare instance?: BudDashboard[`instance`]
-
- /**
- * {@link BudDashboard.messages}
- */
- public declare messages: BudDashboard[`messages`]
-
- /**
- * Restore console function
- *
- * @remarks
- * Returned from {@link patchConsole} call. Restores
- * the normal {@link console} behavior.
- */
- public declare restore?: () => any
-
/**
* {@link BudDashboard.stats}
*/
public declare stats?: StatsCompilation
- /**
- * {@link BudDashboard.status}
- */
- public declare status?: false | string
-
- /**
- * {@link BudDashboard.stderr}
- */
- public stderr = stderr
-
- /**
- * {@link BudDashboard.stdin}
- */
- public stdin = stdin
-
- /**
- * {@link BudDashboard.stdout}
- */
- public stdout = stdout
-
/**
* Class constructor
* @param app
*/
public constructor(app: () => Bud) {
super(app)
-
- this.stdin = this.app.context.stdin ?? stdin
- this.stdout = this.app.context.stdout ?? stdout
- this.stderr = this.app.context.stderr ?? stderr
-
this.formatStatsErrors = makeErrorFormatter(this.app)
- this.render()
- }
-
- /**
- * {@link BudDashboard.patched}
- */
- @bind
- public patched() {
- return !isUndefined(this.restore)
}
/**
* {@link BudDashboard.render}
*/
@bind
- public render(error?: Error) {
+ public render(stats?: StatsCompilation, error?: Error) {
+ if (!stats) return <>No stats>
+
+ const jsonStats = stats.toJson()
+ const stringStats = stats.toString()
+
/**
* Do not render if:
- * - dashboard is disabled
* - CI is enabled
* - silent mode is enabled
*/
- if (
- this.app.context.silent === true ||
- this.app.context.dashboard === false ||
- this.app.context.ci
- )
- return
+ if (this.app.context.silent === true || this.app.context.ci) return
+
/**
- * Set the render function
- *
- * `rerender` is a method returned by the {@link render} function
- * that allows us to update the application without
- * re-mounting it.
- *
- * If `rerender` is not available, we can use the {@link render} function
- * directly (it is hopefully the initial render)
+ * Render string if dashboard is disabled
*/
- const renderApplication = this.instance?.rerender
- ? this.instance.rerender
- : (node: ReactElement) => {
- this.instance = render(node, {
- // We handle the console ourselves
- patchConsole: false,
- stderr: this.stderr,
- stdin: this.stdin,
- stdout: this.stdout,
- })
- }
+ if (this.app.context.dashboard === false) {
+ this.renderString(stringStats)
+ return this
+ }
/**
* Get compilations
@@ -144,9 +76,9 @@ export class Dashboard extends Service implements BudDashboard {
* case, we want to flatten the children array.
*/
const getCompilations = (): Compilations => {
- if (!this.stats) return []
- if (!this.stats.children?.length) return [this.stats]
- return this.stats.children.flat()
+ if (!jsonStats) return []
+ if (!jsonStats.children?.length) return [jsonStats]
+ return jsonStats.children.flat()
}
/**
@@ -198,20 +130,11 @@ export class Dashboard extends Service implements BudDashboard {
* application.
*/
const App =
- this.stdin.isTTY && this.app.isDevelopment
+ stdin.isTTY && this.app.isDevelopment
? TeletypeApplication
: Application
- /**
- * Render the application
- *
- * @remarks
- * The application is rendered using Ink. The `renderApplication`
- * function is used to update the application without re-mounting
- * it. This allows us to update the application without losing
- * the current state.
- */
- renderApplication(
+ return (
@@ -228,13 +151,13 @@ export class Dashboard extends Service implements BudDashboard {
error={error}
isolated={0}
mode={this.app.mode}
+ notifier={this.app.notifier}
proxy={this.app.server?.enabledMiddleware?.[`proxy`]}
proxyUrl={this.app.server?.proxyUrl}
publicDevUrl={this.app.server?.publicUrl}
publicProxyUrl={this.app.server?.publicProxyUrl}
- status={this.status}
/>
- ,
+
)
}
@@ -242,38 +165,9 @@ export class Dashboard extends Service implements BudDashboard {
* {@link BudDashboard.renderString}
*/
@bind
- public renderString(text: string) {
- if (this.app.context.silent) return
- this.stdout.write(`${text}\n`)
- }
-
- /**
- * {@link BudDashboard.updateStats}
- */
- @bind
- public updateStats(stats: StatsCompilation): BudDashboard {
- if (!this.app.compiler) return this
- if (stats && `hash` in stats && stats.hash === this.stats?.hash)
- return this
-
- if (stats) this.stats = stats
-
- if (this.app.context.silent === true) return this
-
- if (
- (this.app.compiler?.stats && this.app.context.dashboard === false) ||
- this.app.context.ci
- ) {
- this.renderString(
- this.app.compiler.stats.toString({
- color: true,
- preset: `minimal`,
- }),
- )
- return this
- }
-
- this.render()
+ public renderString(text: string): Dashboard {
+ if (this.app.context.silent) return this
+ stdout.write(`${text}\n`)
return this
}
}
diff --git a/sources/@roots/bud-dashboard/src/views/server.tsx b/sources/@roots/bud-dashboard/src/views/server.tsx
index b9d2d35a01..102db8ed17 100644
--- a/sources/@roots/bud-dashboard/src/views/server.tsx
+++ b/sources/@roots/bud-dashboard/src/views/server.tsx
@@ -35,7 +35,7 @@ export const Server = ({
return (
{` `}
- Network
+ {` `}Network
{` `}
{
it(`should be a Service`, async () => {
expect(dashboard).toBeInstanceOf(Service)
})
-
- it(`should return early from dashboard.update when there are no stats provided`, async () => {
- dashboard.updateStats(
- // @ts-ignore
- undefined,
- )
- expect(dashboard.stats).toBeUndefined()
- })
})
diff --git a/sources/@roots/bud-framework/src/context.ts b/sources/@roots/bud-framework/src/context.ts
index 8228a611c9..854e8441ec 100644
--- a/sources/@roots/bud-framework/src/context.ts
+++ b/sources/@roots/bud-framework/src/context.ts
@@ -434,6 +434,11 @@ export interface Context {
*/
reload?: boolean
+ /**
+ * Ink render function
+ */
+ render: (element: any) => void
+
/**
* Root bud.js instance
*
diff --git a/sources/@roots/bud-framework/src/notifier.ts b/sources/@roots/bud-framework/src/notifier.ts
index d114fceda4..465ae35c50 100644
--- a/sources/@roots/bud-framework/src/notifier.ts
+++ b/sources/@roots/bud-framework/src/notifier.ts
@@ -166,7 +166,6 @@ export class Notifier {
*/
public async openBrowser(url: string) {
if (!this.app.isDevelopment) return
- if (!this.openBrowserEnabled) return
if (!isString(url)) return
if (this.browserOpened) return
@@ -179,13 +178,6 @@ export class Notifier {
return await open(url)
}
- /**
- * True if browser opening is enabled
- */
- public get openBrowserEnabled(): boolean {
- return this.app?.context.browser === true
- }
-
/**
* Open editor on error
*/
diff --git a/sources/@roots/bud-framework/src/registry/events.ts b/sources/@roots/bud-framework/src/registry/events.ts
index 638ca1cc50..a755d5d8ee 100644
--- a/sources/@roots/bud-framework/src/registry/events.ts
+++ b/sources/@roots/bud-framework/src/registry/events.ts
@@ -1,5 +1,5 @@
import type {Bud} from '@roots/bud-framework'
-import type {MultiStats, Stats} from '@roots/bud-framework/config'
+import type {StatsCompilation} from '@roots/bud-framework/config'
export interface Events {
boot: [Bud]
@@ -7,7 +7,7 @@ export interface Events {
'build.after': [Bud]
'build.before': [Bud]
'compiler.before': [Bud]
- 'compiler.done': [Bud, MultiStats & Stats]
+ 'compiler.done': [Bud, StatsCompilation]
'config.after': [Bud]
register: [Bud]
'server.after': [Bud]
diff --git a/sources/@roots/bud-framework/src/services/compiler/index.ts b/sources/@roots/bud-framework/src/services/compiler/index.ts
index 260ec2d751..28d5bb1f16 100644
--- a/sources/@roots/bud-framework/src/services/compiler/index.ts
+++ b/sources/@roots/bud-framework/src/services/compiler/index.ts
@@ -3,7 +3,6 @@ import type {
Configuration,
MultiCompiler,
MultiStats,
- Stats,
StatsCompilation,
} from '@roots/bud-framework/config'
@@ -11,11 +10,6 @@ import type {
* Compiler service
*/
export interface Compiler {
- /**
- * Compilation stats
- */
- compilationStats: StatsCompilation
-
/**
* Returns a {@link WebpackMultiCompiler} instance
*
@@ -54,7 +48,7 @@ export interface Compiler {
/**
* Raw stats
*/
- stats: Stats & MultiStats
+ stats: StatsCompilation
}
export type BudError = {
diff --git a/sources/@roots/bud-framework/src/services/dashboard/index.ts b/sources/@roots/bud-framework/src/services/dashboard/index.ts
index 63402b87ef..9eec5c74b4 100644
--- a/sources/@roots/bud-framework/src/services/dashboard/index.ts
+++ b/sources/@roots/bud-framework/src/services/dashboard/index.ts
@@ -3,15 +3,15 @@ import type {
StatsError,
} from '@roots/bud-framework/config'
-interface PatchedMessage {
- message: string
- stream: 'stderr' | 'stdout'
-}
-
/**
* Dashboard service container
*/
export interface Dashboard {
+ /**
+ * Received stats
+ */
+ stats?: StatsCompilation
+
/**
* Format stats errors
*/
@@ -19,61 +19,13 @@ export interface Dashboard {
errors: StatsError[] | undefined,
) => StatsError[] | undefined
- /**
- * CLI instance
- */
- instance?: {
- rerender: (...args: any[]) => void
- waitUntilExit: () => Promise
- }
-
- /**
- * Intercepted console messages
- */
- messages: Array
-
- /**
- * Returns true if console is patched
- */
- patched: () => boolean
-
/**
* Render function
*/
- render: (error?: Error) => void
+ render: (stats?: StatsCompilation, error?: any) => any
/**
* Render string to stdout
*/
- renderString(stats: string): void
-
- /**
- * Received stats
- */
- stats?: StatsCompilation
-
- /**
- * Received status
- */
- status?: false | string
-
- /**
- * Stderr stream
- */
- stderr: NodeJS.WriteStream & {fd: 2}
-
- /**
- * Stdin stream
- */
- stdin: NodeJS.ReadStream & {fd: 0}
-
- /**
- * Stdout stream
- */
- stdout: NodeJS.WriteStream & {fd: 1}
-
- /**
- * Update the stats
- */
- updateStats(stats?: StatsCompilation): Dashboard
+ renderString(stats: string): any
}
diff --git a/sources/@roots/bud-stylelint/test/stylelintrc-no-extension.test.ts b/sources/@roots/bud-stylelint/test/stylelintrc-no-extension.test.ts
index 456378be9b..d78e5898eb 100644
--- a/sources/@roots/bud-stylelint/test/stylelintrc-no-extension.test.ts
+++ b/sources/@roots/bud-stylelint/test/stylelintrc-no-extension.test.ts
@@ -19,7 +19,7 @@ describe(`bud build with extensionless stylelintrc`, () => {
expect(runtime).toMatch(/│ ◉ js\/runtime\.js\s/)
expect(css).toMatch(/│ ◉ css\/app\.css\s/)
expect(js).toMatch(/│ ◉ js\/app\.js./)
- expect(timings).toMatch(/╰ .*ms 3 modules \[.\/3 modules cached\]/)
+ expect(timings).toMatch(/╰ .*ms \d* modules \[.\/\d* modules cached\]/)
expect(_s).toEqual(``)
expect(_s2).toMatch(/│/)
expect(_s3).toMatch(/│/)
diff --git a/sources/@roots/bud-support/src/ink/index.ts b/sources/@roots/bud-support/src/ink/index.ts
index 2a83d05919..6b2cdd2316 100644
--- a/sources/@roots/bud-support/src/ink/index.ts
+++ b/sources/@roots/bud-support/src/ink/index.ts
@@ -5,6 +5,7 @@ export {default as Text} from './text.js'
export {
Box,
+ type Instance,
Newline,
render,
type RenderOptions,
diff --git a/sources/@roots/bud/src/cli/commands/build/development/index.tsx b/sources/@roots/bud/src/cli/commands/build/development/index.tsx
index 4b48f30e45..1bb9ffb60e 100644
--- a/sources/@roots/bud/src/cli/commands/build/development/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/build/development/index.tsx
@@ -37,14 +37,14 @@ export default class BuildDevelopmentCommand extends BuildCommand {
],
})
+ public override mode: typeof mode = `development`
+
public browser = browser
public hot = hot
public indicator = indicator
- public override mode: typeof mode = `development`
-
public overlay = overlay
public port = port
diff --git a/sources/@roots/bud/src/cli/commands/clean/index.tsx b/sources/@roots/bud/src/cli/commands/clean/index.tsx
index f889c5522c..22d1a8ba9f 100644
--- a/sources/@roots/bud/src/cli/commands/clean/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/clean/index.tsx
@@ -50,7 +50,7 @@ export default class BudCleanCommand extends BudCommand {
.filter(this.filterCompiler)
.map(async child => {
await this.bud.fs.remove(child.cache.cacheDirectory)
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {child.cache.cacheDirectory}
@@ -63,7 +63,7 @@ export default class BudCleanCommand extends BudCommand {
await this.bud.fs.remove(this.bud.path(`@os-cache`))
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {this.bud.cache.cacheDirectory}
@@ -73,7 +73,7 @@ export default class BudCleanCommand extends BudCommand {
await this.bud.fs.remove(this.bud.path(`@storage`, `conf`))
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {this.bud.path(`@storage`, `conf`)}
@@ -90,7 +90,7 @@ export default class BudCleanCommand extends BudCommand {
.filter(this.filterCompiler)
.map(async child => {
await this.bud.fs.remove(child.path(`@dist`))
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {child.path(`@dist`)}
,
@@ -100,7 +100,7 @@ export default class BudCleanCommand extends BudCommand {
}
await this.bud.fs.remove(this.bud.path(`@dist`))
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {this.bud.path(`@dist`)}
,
@@ -115,7 +115,7 @@ export default class BudCleanCommand extends BudCommand {
.filter(this.filterCompiler)
.map(async child => {
await this.bud.fs.remove(child.path(`@storage`))
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {child.path(`@storage`)}
@@ -129,7 +129,7 @@ export default class BudCleanCommand extends BudCommand {
await this.bud.fs.remove(this.bud.path(`@storage`))
await this.bud.fs.remove(this.bud.path(`@os-cache`))
- BudCleanCommand.renderStatic(
+ this.renderStatic(
✔ emptied {this.bud.path(`@storage`)}
,
diff --git a/sources/@roots/bud/src/cli/commands/config/index.tsx b/sources/@roots/bud/src/cli/commands/config/index.tsx
index 2d800daa8f..a324faf12e 100644
--- a/sources/@roots/bud/src/cli/commands/config/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/config/index.tsx
@@ -26,7 +26,7 @@ export default class ConfigCommand extends BudCommand {
await this.makeBud()
await this.bud.run()
- ConfigCommand.renderStatic()
- ConfigCommand.renderStatic()
+ this.renderStatic()
+ this.renderStatic()
}
}
diff --git a/sources/@roots/bud/src/cli/commands/doctor/index.tsx b/sources/@roots/bud/src/cli/commands/doctor/index.tsx
index 0d83afa4ae..68f3c40422 100644
--- a/sources/@roots/bud/src/cli/commands/doctor/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/doctor/index.tsx
@@ -75,19 +75,17 @@ for a lot of edge cases so it might return a false positive.
this.timings.build = buildTimer()
if (platform() === `win32`) {
- DoctorCommand.renderStatic()
+ this.renderStatic()
}
const name = this.bud.context.manifest.name
? this.bud.context.manifest.name
: this.bud.path()
- DoctorCommand.renderStatic(
- ,
- )
+ this.renderStatic()
if (this.bud.hasChildren) {
- DoctorCommand.renderStatic(
+ this.renderStatic(
Child compilers{`\n`}
@@ -107,14 +105,14 @@ for a lot of edge cases so it might return a false positive.
)
}
- DoctorCommand.renderStatic(
+ this.renderStatic(
Mode
{this.bud.mode}
,
)
- DoctorCommand.renderStatic(
+ this.renderStatic(
Project paths
{` `}
@@ -142,7 +140,7 @@ for a lot of edge cases so it might return a false positive.
,
)
- DoctorCommand.renderStatic(
+ this.renderStatic(
Checking versions of core packages
{` `}
@@ -164,8 +162,8 @@ for a lot of edge cases so it might return a false positive.
console.error(error)
}
- DoctorCommand.renderStatic()
- DoctorCommand.renderStatic()
+ this.renderStatic()
+ this.renderStatic()
try {
this.entrypoints = this.bud.build.config.entry
@@ -186,13 +184,13 @@ for a lot of edge cases so it might return a false positive.
console.error(error)
}
- DoctorCommand.renderStatic(
+ this.renderStatic(
Enabled extensions{`\n`}
{this.mapExtensions(this.enabledExtensions)}
,
)
- DoctorCommand.renderStatic(
+ this.renderStatic(
Disabled extensions{`\n`}
{this.mapExtensions(this.disabledExtensions)}
@@ -208,7 +206,7 @@ for a lot of edge cases so it might return a false positive.
this.entrypoints[0][1].import[0] === `index` &&
!(await this.bud.fs.exists(this.bud.path(`@src/index.js`)))
) {
- DoctorCommand.renderStatic(
+ this.renderStatic(
Development server
@@ -258,7 +256,7 @@ for a lot of edge cases so it might return a false positive.
try {
webpack.validate(this.bud.build.config)
- DoctorCommand.renderStatic(
+ this.renderStatic(
{figures.tick} webpack validated configuration
@@ -266,7 +264,7 @@ for a lot of edge cases so it might return a false positive.
,
)
} catch (error) {
- DoctorCommand.renderStatic(
+ this.renderStatic(
❌ {error?.message ?? error}
,
diff --git a/sources/@roots/bud/src/cli/commands/env/index.tsx b/sources/@roots/bud/src/cli/commands/env/index.tsx
index e099f1711a..432c5074e3 100644
--- a/sources/@roots/bud/src/cli/commands/env/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/env/index.tsx
@@ -26,6 +26,6 @@ export default class EnvCommand extends BudCommand {
await this.makeBud()
await this.bud.run()
- EnvCommand.renderStatic()
+ this.renderStatic()
}
}
diff --git a/sources/@roots/bud/src/cli/commands/index.tsx b/sources/@roots/bud/src/cli/commands/index.tsx
index 5c830ca485..8adeb379f0 100644
--- a/sources/@roots/bud/src/cli/commands/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/index.tsx
@@ -28,7 +28,7 @@ import {Command, Option} from '@roots/bud-support/clipanion'
import {bind} from '@roots/bud-support/decorators/bind'
import {BudError} from '@roots/bud-support/errors'
import figures from '@roots/bud-support/figures'
-import {Box, render, Static} from '@roots/bud-support/ink'
+import * as Ink from '@roots/bud-support/ink'
import isNumber from '@roots/bud-support/lodash/isNumber'
import logger from '@roots/bud-support/logger'
import args from '@roots/bud-support/utilities/args'
@@ -79,17 +79,6 @@ export default class BudCommand extends Command {
examples: [[`Interactive menu of available subcommands`, `$0`]],
})
- /**
- * Render static
- */
- public static renderStatic(...children: Array) {
- return render(
-
- {(child, id) => {child}}
- ,
- ).unmount()
- }
-
public basedir = basedir
public declare bud?: Bud | undefined
@@ -122,6 +111,35 @@ export default class BudCommand extends Command {
public verbose = verbose
+ public ink?: Ink.Instance
+
+ /**
+ * Ink {@link Instance}
+ */
+ public static Ink: typeof Ink = Ink
+
+ /**
+ * Render cli output
+ */
+ public render(El: React.ReactElement) {
+ if (this.ink) {
+ this.ink.rerender(El)
+ } else {
+ this.ink = BudCommand.Ink.render(El)
+ }
+ }
+
+ /**
+ * Render static cli output
+ */
+ public renderStatic(...children: Array) {
+ return this.render(
+
+ {(child, id) => {child}}
+ ,
+ )
+ }
+
/**
* Execute arbitrary sh command with inherited stdio
*/
@@ -274,24 +292,11 @@ export default class BudCommand extends Command {
}
}
- if (this.bud?.dashboard?.instance) {
- this.bud.dashboard.render(error)
-
- if (this.bud.isProduction) {
- const unmountDashboard = async () =>
- await this.bud.dashboard.instance.waitUntilExit()
-
- this.bud.compiler?.instance?.close
- ? this.bud.compiler.instance.close(unmountDashboard)
- : await unmountDashboard()
- }
- } else {
- BudCommand.renderStatic(
-
-
- ,
- )
- }
+ this.renderStatic(
+
+
+ ,
+ )
// fallthrough
if (!this.bud || this.bud?.isProduction) exit(1)
@@ -301,7 +306,7 @@ export default class BudCommand extends Command {
* {@link Command.execute}
*/
public async execute(): Promise {
- render()
+ this.render()
}
/**
@@ -324,6 +329,7 @@ export default class BudCommand extends Command {
this.context.dry = this.dry
this.context.mode = this.mode ?? this.context.mode ?? `production`
this.context.silent = this.silent
+ this.context.render = this.render
this.bud = instance.get()
diff --git a/sources/@roots/bud/src/cli/commands/view/index.tsx b/sources/@roots/bud/src/cli/commands/view/index.tsx
index eac4248950..bae2216777 100644
--- a/sources/@roots/bud/src/cli/commands/view/index.tsx
+++ b/sources/@roots/bud/src/cli/commands/view/index.tsx
@@ -43,7 +43,7 @@ export default class BudViewCommand extends BudCommand {
if (this.color) value = highlight(value)
- BudViewCommand.renderStatic(
+ this.renderStatic(
{this.subject ?? `build.config`}
{` `}
diff --git a/sources/@roots/bud/src/context/index.ts b/sources/@roots/bud/src/context/index.ts
index 57c1b741e2..7b6346a6e0 100644
--- a/sources/@roots/bud/src/context/index.ts
+++ b/sources/@roots/bud/src/context/index.ts
@@ -13,6 +13,7 @@ import * as projectPaths from '@roots/bud-support/utilities/paths'
import * as budManifest from './bud.js'
import getExtensions from './extensions.js'
import services from './services.js'
+import {render} from '@roots/bud-support/ink'
export type Options = Omit, `extensions`> & {
extensions?: Array
@@ -53,6 +54,7 @@ export default async (options: Options = {}): Promise => {
manifest: {...(manifest ?? {}), ...(options?.manifest ?? {})},
mode: options?.mode ?? `production`,
paths: {...paths, ...(options?.paths ?? {})},
+ render: options?.render ?? render,
services: [...(services ?? []), ...(options?.services ?? [])],
stderr: options?.stderr ?? stderr,
stdin: options?.stdin ?? stdin,