Skip to content

Commit 6c4dd60

Browse files
refactor(ui): implement version with minimal core impact
- No changes made to runWebpack, serve, build, or watch logic. - No Pagination and legacy terminal output flow remain untouched.
1 parent 1280f74 commit 6c4dd60

File tree

2 files changed

+193
-8
lines changed

2 files changed

+193
-8
lines changed

packages/webpack-cli/src/ui-renderer.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ export interface CommandHelpData {
5959
globalOptions: HelpOption[];
6060
}
6161

62+
/** Passed to `renderCommandHeader` to describe the running command. */
63+
export interface CommandMeta {
64+
name: string;
65+
description: string;
66+
}
67+
68+
/** One section emitted by `renderInfoOutput`, e.g. "System" or "Binaries". */
69+
export interface InfoSection {
70+
title: string;
71+
rows: Row[];
72+
}
73+
6274
// ─── Layout constants ─────────────────────────────────────────────
6375
export const MAX_WIDTH = 80;
6476
export const INDENT = 2;
@@ -127,6 +139,29 @@ export function renderRows(
127139
}
128140
}
129141

142+
export function renderCommandHeader(meta: CommandMeta, opts: RenderOptions): void {
143+
const { colors, log } = opts;
144+
const termWidth = Math.min(opts.columns || MAX_WIDTH, MAX_WIDTH);
145+
146+
log("");
147+
log(`${indent(INDENT)}${colors.bold(colors.cyan("⬡"))} ${colors.bold(`webpack ${meta.name}`)}`);
148+
log(divider(termWidth, colors));
149+
150+
if (meta.description) {
151+
const descWidth = termWidth - INDENT * 2;
152+
for (const line of wrapValue(meta.description, descWidth)) {
153+
log(`${indent(INDENT)}${line}`);
154+
}
155+
log("");
156+
}
157+
}
158+
159+
export function renderCommandFooter(opts: RenderOptions): void {
160+
const termWidth = Math.min(opts.columns, MAX_WIDTH);
161+
opts.log(divider(termWidth, opts.colors));
162+
opts.log("");
163+
}
164+
130165
function _renderHelpOptions(
131166
options: HelpOption[],
132167
colors: Colors,
@@ -236,6 +271,108 @@ export function renderAliasHelp(data: AliasHelpData, opts: RenderOptions): void
236271
renderOptionHelp(data.optionHelp, opts);
237272
}
238273

274+
export function renderError(message: string, opts: RenderOptions): void {
275+
const { colors, log } = opts;
276+
log(`${indent(INDENT)}${colors.red("✖")} ${colors.bold(message)}`);
277+
}
278+
279+
export function renderSuccess(message: string, opts: RenderOptions): void {
280+
const { colors, log } = opts;
281+
log(`${indent(INDENT)}${colors.green("✔")} ${colors.bold(message)}`);
282+
}
283+
284+
export function renderWarning(message: string, opts: RenderOptions): void {
285+
const { colors, log } = opts;
286+
log(`${indent(INDENT)}${colors.yellow("⚠")} ${message}`);
287+
}
288+
289+
export function renderInfo(message: string, opts: RenderOptions): void {
290+
const { colors, log } = opts;
291+
log(`${indent(INDENT)}${colors.cyan("ℹ")} ${message}`);
292+
}
293+
294+
export function parseEnvinfoSections(raw: string): InfoSection[] {
295+
const sections: InfoSection[] = [];
296+
let current: InfoSection | null = null;
297+
298+
for (const line of raw.split("\n")) {
299+
const sectionMatch = line.match(/^ {2}([^:]+):\s*$/);
300+
if (sectionMatch) {
301+
if (current) sections.push(current);
302+
current = { title: sectionMatch[1].trim(), rows: [] };
303+
continue;
304+
}
305+
306+
const rowMatch = line.match(/^ {4}([^:]+):\s+(.+)$/);
307+
if (rowMatch && current) {
308+
current.rows.push({ label: rowMatch[1].trim(), value: rowMatch[2].trim() });
309+
continue;
310+
}
311+
312+
const emptyRowMatch = line.match(/^ {4}([^:]+):\s*$/);
313+
if (emptyRowMatch && current) {
314+
current.rows.push({ label: emptyRowMatch[1].trim(), value: "N/A", color: (str) => str });
315+
}
316+
}
317+
318+
if (current) sections.push(current);
319+
return sections.filter((section) => section.rows.length > 0);
320+
}
321+
322+
export function renderInfoOutput(rawEnvinfo: string, opts: RenderOptions): void {
323+
const { colors, log } = opts;
324+
const termWidth = Math.min(opts.columns, MAX_WIDTH);
325+
const div = divider(termWidth, colors);
326+
const sections = parseEnvinfoSections(rawEnvinfo);
327+
328+
log("");
329+
330+
for (const section of sections) {
331+
log(
332+
`${indent(INDENT)}${colors.bold(colors.cyan("⬡"))} ${colors.bold(colors.cyan(section.title))}`,
333+
);
334+
log(div);
335+
renderRows(section.rows, colors, log, termWidth);
336+
log(div);
337+
log("");
338+
}
339+
}
340+
341+
export function renderVersionOutput(rawEnvinfo: string, opts: RenderOptions): void {
342+
const { colors, log } = opts;
343+
const termWidth = Math.min(opts.columns, MAX_WIDTH);
344+
const div = divider(termWidth, colors);
345+
const sections = parseEnvinfoSections(rawEnvinfo);
346+
347+
for (const section of sections) {
348+
log("");
349+
log(
350+
`${indent(INDENT)}${colors.bold(colors.cyan("⬡"))} ${colors.bold(colors.cyan(section.title))}`,
351+
);
352+
log(div);
353+
354+
const labelWidth = Math.max(...section.rows.map((row) => row.label.length));
355+
356+
for (const { label, value } of section.rows) {
357+
const arrowIdx = value.indexOf("=>");
358+
359+
if (arrowIdx !== -1) {
360+
const requested = value.slice(0, arrowIdx).trim();
361+
const resolved = value.slice(arrowIdx + 2).trim();
362+
log(
363+
`${indent(INDENT)}${colors.bold(label.padEnd(labelWidth))}${indent(COL_GAP)}` +
364+
`${colors.cyan(requested.padEnd(12))} ${colors.cyan("→")} ${colors.green(colors.bold(resolved))}`,
365+
);
366+
} else {
367+
log(
368+
`${indent(INDENT)}${colors.bold(label.padEnd(labelWidth))}${indent(COL_GAP)}${colors.green(value)}`,
369+
);
370+
}
371+
}
372+
log(div);
373+
}
374+
}
375+
239376
export function renderFooter(opts: RenderOptions, footer: FooterOptions = {}): void {
240377
const { colors, log } = opts;
241378

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,16 @@ import {
3636
HelpOption,
3737
RenderOptions,
3838
renderAliasHelp,
39+
renderCommandFooter,
40+
renderCommandHeader,
3941
renderCommandHelp,
42+
renderError,
4043
renderFooter,
44+
renderInfoOutput,
4145
renderOptionHelp,
46+
renderSuccess,
47+
renderVersionOutput,
48+
renderWarning,
4249
} from "./ui-renderer.js";
4350

4451
const WEBPACK_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_PACKAGE);
@@ -1824,9 +1831,28 @@ class WebpackCLI {
18241831
},
18251832
],
18261833
action: async (options: { output?: string }) => {
1827-
const info = await this.#renderVersion(options);
1834+
const renderOpts = this.#renderOptions();
18281835

1829-
this.logger.raw(info);
1836+
if (options.output) {
1837+
// Machine-readable output requested, bypass the visual renderer entirely.
1838+
const info = await this.#renderVersion(options);
1839+
this.logger.raw(info);
1840+
return;
1841+
}
1842+
1843+
renderCommandHeader(
1844+
{ name: "version", description: "Installed package versions." },
1845+
renderOpts,
1846+
);
1847+
1848+
const rawInfo = await this.#getInfoOutput({
1849+
information: {
1850+
npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`,
1851+
},
1852+
});
1853+
1854+
renderVersionOutput(rawInfo, renderOpts);
1855+
renderFooter(renderOpts);
18301856
},
18311857
},
18321858
info: {
@@ -1857,9 +1883,23 @@ class WebpackCLI {
18571883
},
18581884
],
18591885
action: async (options: { output?: string; additionalPackage?: string[] }) => {
1886+
const renderOpts = this.#renderOptions();
1887+
1888+
if (!options.output) {
1889+
renderCommandHeader(
1890+
{ name: "info", description: "System and environment information." },
1891+
renderOpts,
1892+
);
1893+
}
1894+
18601895
const info = await this.#getInfoOutput(options);
18611896

1862-
this.logger.raw(info);
1897+
if (options.output) {
1898+
this.logger.raw(info);
1899+
return;
1900+
}
1901+
1902+
renderInfoOutput(info, renderOpts);
18631903
},
18641904
},
18651905
configtest: {
@@ -1881,6 +1921,7 @@ class WebpackCLI {
18811921
configPath ? { env, argv, webpack, config: [configPath] } : { env, argv, webpack },
18821922
);
18831923
const configPaths = new Set<string>();
1924+
const renderOpts = this.#renderOptions();
18841925

18851926
if (Array.isArray(config.options)) {
18861927
for (const options of config.options) {
@@ -1899,25 +1940,32 @@ class WebpackCLI {
18991940
}
19001941

19011942
if (configPaths.size === 0) {
1902-
this.logger.error("No configuration found.");
1943+
renderError("No configuration found.", renderOpts);
19031944
process.exit(2);
19041945
}
19051946

1906-
this.logger.info(`Validate '${[...configPaths].join(" ,")}'.`);
1947+
renderCommandHeader(
1948+
{ name: "configtest", description: "Validating your webpack configuration." },
1949+
renderOpts,
1950+
);
1951+
1952+
const pathList = [...configPaths].join(", ");
1953+
renderWarning(`Validating: ${pathList}`, renderOpts);
19071954

19081955
try {
19091956
cmd.context.webpack.validate(config.options);
19101957
} catch (error) {
19111958
if (this.isValidationError(error as Error)) {
1912-
this.logger.error((error as Error).message);
1959+
renderError((error as Error).message, renderOpts);
19131960
} else {
1914-
this.logger.error(error);
1961+
renderError(String(error), renderOpts);
19151962
}
19161963

19171964
process.exit(2);
19181965
}
19191966

1920-
this.logger.success("There are no validation errors in the given webpack configuration.");
1967+
renderSuccess("No validation errors found.", renderOpts);
1968+
renderCommandFooter(renderOpts);
19211969
},
19221970
},
19231971
};

0 commit comments

Comments
 (0)