@emulsify/core 4.3.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main-vite.js +11 -1
- package/.storybook/main.js +20 -0
- package/.storybook/ready-reporter.js +230 -0
- package/README.md +1 -0
- package/config/vite/plugins/assets/copy-src-assets.js +69 -25
- package/config/vite/plugins/assets/copy-twig-files.js +76 -30
- package/config/vite/plugins/reporter/diagnostics.js +1 -0
- package/config/vite/plugins/reporter/format.js +41 -0
- package/config/vite/plugins/reporter/index.js +114 -5
- package/config/vite/plugins/reporter/render.js +613 -39
- package/config/vite/plugins/reporter/source-roots.js +561 -0
- package/config/vite/plugins/reporter/verbosity.js +119 -0
- package/config/vite/plugins/reporter/vite-logger.js +66 -5
- package/config/vite/vite.config.js +44 -4
- package/package.json +5 -2
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
SYMBOLS,
|
|
13
13
|
displayLocation,
|
|
14
14
|
displayPath,
|
|
15
|
+
formatBytes,
|
|
15
16
|
formatClockTime,
|
|
16
17
|
formatDuration,
|
|
18
|
+
formatPreciseBytes,
|
|
17
19
|
platformLabel,
|
|
18
20
|
pluralize,
|
|
19
21
|
} from './format.js';
|
|
20
22
|
import { deprecationFix, deprecationMigrator } from './sass-logger.js';
|
|
23
|
+
import { sharedRootPath } from './source-roots.js';
|
|
21
24
|
|
|
22
25
|
const INDENT = ' ';
|
|
23
26
|
const DETAIL_INDENT = ' ';
|
|
@@ -100,36 +103,270 @@ const WORDMARK = [
|
|
|
100
103
|
* easy to scroll past; a block of art is not. Terminals that cannot render the
|
|
101
104
|
* glyphs get the plain name instead of mojibake.
|
|
102
105
|
*
|
|
106
|
+
* The banner carries only the version. It is emitted from `configResolved`,
|
|
107
|
+
* before the build has run, so it cannot know what was written to `dist/` — and
|
|
108
|
+
* splitting the project facts across two moments would mean reading the input
|
|
109
|
+
* roots in one place and the output tally in another. They belong together, so
|
|
110
|
+
* both live in the facts block that {@link renderFacts} prints with the summary.
|
|
111
|
+
*
|
|
103
112
|
* @param {{
|
|
104
113
|
* version?: string,
|
|
105
|
-
* platform?: string,
|
|
106
|
-
* entryCount?: number,
|
|
107
114
|
* unicode?: boolean,
|
|
108
115
|
* styler: (format: string|string[], text: string) => string
|
|
109
116
|
* }} options - Banner inputs.
|
|
110
117
|
* @returns {string[]} Banner lines.
|
|
111
118
|
*/
|
|
112
|
-
export function renderBanner({
|
|
113
|
-
|
|
119
|
+
export function renderBanner({ version, unicode = true, styler }) {
|
|
120
|
+
const mark = unicode
|
|
121
|
+
? WORDMARK.map((row) => `${INDENT}${styler(['bold', 'cyan'], row)}`)
|
|
122
|
+
: [`${INDENT}${styler(['bold', 'cyan'], 'EMULSIFY')}`];
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
'',
|
|
126
|
+
...mark,
|
|
127
|
+
`${INDENT}${styler('gray', `core ${version || '0.0.0'}`)}`,
|
|
128
|
+
'',
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Labels for the rows in the facts block.
|
|
134
|
+
*
|
|
135
|
+
* @type {{platform: string, input: string, output: string}}
|
|
136
|
+
*/
|
|
137
|
+
const FACT_LABELS = {
|
|
138
|
+
platform: 'platform',
|
|
139
|
+
input: 'input',
|
|
140
|
+
output: 'output',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Render the project facts block.
|
|
145
|
+
*
|
|
146
|
+
* The `input` rows are the reason this block exists. A total entry count cannot
|
|
147
|
+
* distinguish a healthy project from one whose second source root was never
|
|
148
|
+
* discovered, so each root is named with what it contributed. A configured root
|
|
149
|
+
* reporting zero is reported rather than hidden — that row is usually the bug.
|
|
150
|
+
*
|
|
151
|
+
* @param {{
|
|
152
|
+
* platform?: string,
|
|
153
|
+
* inputRows?: Array<{name: string, path: string, count: number}>,
|
|
154
|
+
* outDir?: string,
|
|
155
|
+
* write?: {fileCount: number, totalBytes: number, largest?: {fileName: string, bytes: number}},
|
|
156
|
+
* styler: (format: string|string[], text: string) => string
|
|
157
|
+
* }} options - Facts inputs.
|
|
158
|
+
* @returns {string[]} Facts lines.
|
|
159
|
+
*/
|
|
160
|
+
export function renderFacts({
|
|
114
161
|
platform,
|
|
115
|
-
|
|
162
|
+
inputRows = [],
|
|
163
|
+
outDir = 'dist',
|
|
164
|
+
write,
|
|
165
|
+
styler,
|
|
166
|
+
}) {
|
|
167
|
+
const labelWidth = Math.max(
|
|
168
|
+
...Object.values(FACT_LABELS).map((label) => label.length),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Render one labelled row, or a continuation row when the label repeats.
|
|
173
|
+
*
|
|
174
|
+
* @param {string|undefined} label - Row label, omitted for continuations.
|
|
175
|
+
* @param {string} value - Rendered value.
|
|
176
|
+
* @returns {string} Finished line.
|
|
177
|
+
*/
|
|
178
|
+
const row = (label, value) =>
|
|
179
|
+
`${DETAIL_INDENT}${styler('gray', (label || '').padEnd(labelWidth))} ${value}`;
|
|
180
|
+
|
|
181
|
+
const lines = [row(FACT_LABELS.platform, platformLabel(platform))];
|
|
182
|
+
|
|
183
|
+
// Paths are padded to a shared width and counts are right-aligned on their
|
|
184
|
+
// digits, so both the paths and the numbers read as columns however many roots
|
|
185
|
+
// a project declares. Padding is applied before styling because ANSI escapes
|
|
186
|
+
// carry no display width and would skew every row by a different amount.
|
|
187
|
+
if (inputRows.length > 0) {
|
|
188
|
+
const pathWidth = Math.max(...inputRows.map((entry) => entry.path.length));
|
|
189
|
+
const countWidth = Math.max(
|
|
190
|
+
...inputRows.map((entry) => String(entry.count).length),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
inputRows.forEach((entry, index) => {
|
|
194
|
+
// An overflow row names a count of directories rather than a directory, so
|
|
195
|
+
// it is dimmed to keep it from reading as a path.
|
|
196
|
+
const path = styler(
|
|
197
|
+
entry.overflow ? 'gray' : 'cyan',
|
|
198
|
+
entry.path.padEnd(pathWidth),
|
|
199
|
+
);
|
|
200
|
+
const noun = entry.count === 1 ? 'entry' : 'entries';
|
|
201
|
+
const count = styler(
|
|
202
|
+
// A configured root that matched nothing is the row most likely to be a
|
|
203
|
+
// misconfiguration, so it is the one row here that is not dim.
|
|
204
|
+
entry.count === 0 ? 'yellow' : 'gray',
|
|
205
|
+
`${String(entry.count).padStart(countWidth)} ${noun}`,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
lines.push(
|
|
209
|
+
row(index === 0 ? FACT_LABELS.input : '', `${path} ${count}`),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const outputFacts = [];
|
|
215
|
+
if (write) {
|
|
216
|
+
outputFacts.push(pluralize(write.fileCount, 'file'));
|
|
217
|
+
outputFacts.push(formatBytes(write.totalBytes));
|
|
218
|
+
|
|
219
|
+
if (write.largest) {
|
|
220
|
+
outputFacts.push(
|
|
221
|
+
`largest ${write.largest.fileName} ${formatBytes(write.largest.bytes)}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const outputSuffix =
|
|
227
|
+
outputFacts.length > 0
|
|
228
|
+
? styler('gray', ` ${outputFacts.join(SEPARATOR)}`)
|
|
229
|
+
: '';
|
|
230
|
+
|
|
231
|
+
lines.push(row(FACT_LABELS.output, `${outDir}${outputSuffix}`));
|
|
232
|
+
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Labels for the URL rows printed beneath a ready headline.
|
|
238
|
+
*
|
|
239
|
+
* @type {{local: string, network: string}}
|
|
240
|
+
*/
|
|
241
|
+
const URL_LABELS = {
|
|
242
|
+
local: 'local',
|
|
243
|
+
network: 'network',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Render the ready state for a long-running service.
|
|
248
|
+
*
|
|
249
|
+
* Storybook announces itself with a boxed banner drawn in its own visual
|
|
250
|
+
* language, which reads as a second tool's output rather than part of the
|
|
251
|
+
* build. This renders the same facts in the reporter's vocabulary so one
|
|
252
|
+
* `develop` run looks like one tool.
|
|
253
|
+
*
|
|
254
|
+
* Kept pure and service-agnostic so both callers share it: the Storybook
|
|
255
|
+
* preset that runs while `concurrently` owns the terminal, and any launcher
|
|
256
|
+
* that owns both child processes and prints a combined block.
|
|
257
|
+
*
|
|
258
|
+
* A port that does not match the one requested is reported rather than
|
|
259
|
+
* silently accepted. Storybook falls forward to the next free port, so the
|
|
260
|
+
* difference usually means a previous session is still running — and a browser
|
|
261
|
+
* pointed at the requested port would then be showing a stale instance.
|
|
262
|
+
*
|
|
263
|
+
* @param {{
|
|
264
|
+
* service?: string,
|
|
265
|
+
* urls?: {local?: string, network?: string},
|
|
266
|
+
* durationMs?: number,
|
|
267
|
+
* portDrift?: {requested: number|string, actual: number|string},
|
|
268
|
+
* styler: (format: string|string[], text: string) => string
|
|
269
|
+
* }} options - Ready-state inputs.
|
|
270
|
+
* @returns {string[]} Ready lines.
|
|
271
|
+
*/
|
|
272
|
+
export function renderReady({
|
|
273
|
+
service = 'storybook',
|
|
274
|
+
urls = {},
|
|
275
|
+
durationMs,
|
|
276
|
+
portDrift,
|
|
116
277
|
unicode = true,
|
|
117
278
|
styler,
|
|
118
279
|
}) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
`Platform: ${platformLabel(platform)}`,
|
|
122
|
-
];
|
|
280
|
+
const drifted =
|
|
281
|
+
portDrift && String(portDrift.requested) !== String(portDrift.actual);
|
|
123
282
|
|
|
124
|
-
|
|
125
|
-
|
|
283
|
+
const facts = [];
|
|
284
|
+
if (Number.isFinite(durationMs)) facts.push(formatDuration(durationMs));
|
|
285
|
+
if (drifted) {
|
|
286
|
+
facts.push(`port ${portDrift.requested} in use, using ${portDrift.actual}`);
|
|
126
287
|
}
|
|
127
288
|
|
|
128
|
-
const
|
|
129
|
-
?
|
|
130
|
-
:
|
|
289
|
+
const symbol = drifted
|
|
290
|
+
? styler('yellow', SYMBOLS.warning)
|
|
291
|
+
: styler('green', SYMBOLS.ok);
|
|
292
|
+
const headline = drifted
|
|
293
|
+
? styler('yellow', `${service} ready`)
|
|
294
|
+
: `${service} ready`;
|
|
295
|
+
const suffix =
|
|
296
|
+
facts.length > 0 ? styler('gray', SEPARATOR + facts.join(SEPARATOR)) : '';
|
|
297
|
+
|
|
298
|
+
const lines = [`${INDENT}${symbol} ${headline}${suffix}`];
|
|
299
|
+
|
|
300
|
+
const rows = Object.entries(URL_LABELS).filter(([key]) => urls[key]);
|
|
301
|
+
if (rows.length === 0) return lines;
|
|
302
|
+
|
|
303
|
+
// Padding is applied before styling so ANSI escapes never skew the columns.
|
|
304
|
+
const labelWidth = Math.max(...rows.map(([, label]) => label.length));
|
|
305
|
+
|
|
306
|
+
const body = rows.map(
|
|
307
|
+
([key, label]) =>
|
|
308
|
+
`${INDENT}${INDENT}${styler('gray', label.padEnd(labelWidth))} ${styler(['bold', 'cyan'], urls[key])}`,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// The rules are measured from the longest row rather than fixed, so a long
|
|
312
|
+
// network address or an added row cannot punch through the panel. Width is
|
|
313
|
+
// taken from the unstyled text because ANSI escapes carry no display width.
|
|
314
|
+
const width =
|
|
315
|
+
Math.max(
|
|
316
|
+
...rows.map(
|
|
317
|
+
([key, label]) =>
|
|
318
|
+
INDENT.length * 2 +
|
|
319
|
+
label.padEnd(labelWidth).length +
|
|
320
|
+
3 +
|
|
321
|
+
urls[key].length,
|
|
322
|
+
),
|
|
323
|
+
) - INDENT.length;
|
|
324
|
+
|
|
325
|
+
lines.push('');
|
|
326
|
+
lines.push(...renderPanel(body, width, drifted, unicode, styler));
|
|
327
|
+
// Storybook keeps logging after it announces itself — timing lines, and under
|
|
328
|
+
// `--ci` a migration notice or two. Closing with a blank line stops those from
|
|
329
|
+
// butting straight up against the panel's lower rule.
|
|
330
|
+
lines.push('');
|
|
131
331
|
|
|
132
|
-
return
|
|
332
|
+
return lines;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Frame a block of rows between two half-block rules.
|
|
337
|
+
*
|
|
338
|
+
* Storybook draws its ready state in a rounded box, which reads as a second
|
|
339
|
+
* tool's output rather than as part of the build. The rules here are built from
|
|
340
|
+
* the same half-block glyphs as the wordmark, so the panel belongs to Emulsify's
|
|
341
|
+
* visual language instead of importing another tool's.
|
|
342
|
+
*
|
|
343
|
+
* The glyph choice is deliberate: `▄` sits on the baseline and `▀` sits at cap
|
|
344
|
+
* height, so the pair encloses the rows without the corner joins that
|
|
345
|
+
* box-drawing characters need — and without the alignment failures those joins
|
|
346
|
+
* produce when a row contains a character the font renders at a different width.
|
|
347
|
+
*
|
|
348
|
+
* Terminals that cannot render block glyphs get the rows alone. The same
|
|
349
|
+
* `supportsUnicode()` gate gives the wordmark its plain-text fallback, so a
|
|
350
|
+
* terminal always gets both treatments or neither.
|
|
351
|
+
*
|
|
352
|
+
* @param {string[]} body - Rendered rows to enclose.
|
|
353
|
+
* @param {number} width - Rule width in columns.
|
|
354
|
+
* @param {boolean} warned - Whether the panel reports a problem.
|
|
355
|
+
* @param {boolean} unicode - Whether block glyphs are safe to emit.
|
|
356
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
357
|
+
* @returns {string[]} Panel lines.
|
|
358
|
+
*/
|
|
359
|
+
function renderPanel(body, width, warned, unicode, styler) {
|
|
360
|
+
if (!unicode) return body;
|
|
361
|
+
|
|
362
|
+
const color = warned ? 'yellow' : 'cyan';
|
|
363
|
+
const safeWidth = Math.max(1, Math.round(width));
|
|
364
|
+
|
|
365
|
+
return [
|
|
366
|
+
`${INDENT}${styler(color, '▄'.repeat(safeWidth))}`,
|
|
367
|
+
...body,
|
|
368
|
+
`${INDENT}${styler(color, '▀'.repeat(safeWidth))}`,
|
|
369
|
+
];
|
|
133
370
|
}
|
|
134
371
|
|
|
135
372
|
/**
|
|
@@ -596,13 +833,14 @@ function renderProblems(
|
|
|
596
833
|
assetRows,
|
|
597
834
|
importErrors,
|
|
598
835
|
syntaxErrors,
|
|
836
|
+
unicode = true,
|
|
599
837
|
) {
|
|
600
|
-
const
|
|
838
|
+
const attention = [];
|
|
601
839
|
|
|
602
840
|
const syntaxLines = renderSyntaxErrors(syntaxErrors, styler);
|
|
603
841
|
if (syntaxLines.length > 0) {
|
|
604
|
-
|
|
605
|
-
|
|
842
|
+
attention.push('');
|
|
843
|
+
attention.push(...syntaxLines);
|
|
606
844
|
}
|
|
607
845
|
|
|
608
846
|
const importLines = renderImportErrors(
|
|
@@ -612,41 +850,256 @@ function renderProblems(
|
|
|
612
850
|
styler,
|
|
613
851
|
);
|
|
614
852
|
if (importLines.length > 0) {
|
|
615
|
-
|
|
616
|
-
|
|
853
|
+
attention.push('');
|
|
854
|
+
attention.push(...importLines);
|
|
617
855
|
}
|
|
618
856
|
|
|
619
857
|
if (snapshot.errors.length > 0) {
|
|
620
|
-
|
|
621
|
-
|
|
858
|
+
attention.push('');
|
|
859
|
+
attention.push(
|
|
622
860
|
`${INDENT}${styler('red', SYMBOLS.error)} ${styler('red', pluralize(snapshot.errors.length, 'error'))}`,
|
|
623
861
|
);
|
|
624
|
-
|
|
862
|
+
attention.push(...renderDetailRows(snapshot.errors, projectDir, styler));
|
|
625
863
|
}
|
|
626
864
|
|
|
627
865
|
if (snapshot.warnings.length > 0) {
|
|
628
|
-
|
|
629
|
-
|
|
866
|
+
attention.push('');
|
|
867
|
+
attention.push(
|
|
630
868
|
`${INDENT}${styler('yellow', SYMBOLS.warning)} ${styler('yellow', pluralize(snapshot.warnings.length, 'warning'))}`,
|
|
631
869
|
);
|
|
632
|
-
|
|
870
|
+
attention.push(...renderDetailRows(snapshot.warnings, projectDir, styler));
|
|
633
871
|
}
|
|
634
872
|
|
|
635
873
|
const assetLines = renderUnresolvedAssets(assetRows, styler);
|
|
636
874
|
if (assetLines.length > 0) {
|
|
875
|
+
attention.push('');
|
|
876
|
+
attention.push(...assetLines);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const debt = renderDeprecations(snapshot, projectDir, styler, sourceGlob);
|
|
880
|
+
|
|
881
|
+
const lines = [];
|
|
882
|
+
|
|
883
|
+
// Sass deprecations are inherited debt on almost every project, and there are
|
|
884
|
+
// usually two orders of magnitude more of them than of today's actual
|
|
885
|
+
// breakages. Without the split, 190 deprecations and six broken asset URLs
|
|
886
|
+
// compete for the same attention; with it, the reader knows which block is
|
|
887
|
+
// about the edit they just made.
|
|
888
|
+
//
|
|
889
|
+
// A divider is only drawn when its section has content. Labelling an empty
|
|
890
|
+
// category advertises a problem the project does not have.
|
|
891
|
+
if (attention.length > 0) {
|
|
892
|
+
lines.push('', renderDivider('needs attention', unicode, styler));
|
|
893
|
+
lines.push(...attention);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (debt.length > 0) {
|
|
897
|
+
lines.push('', renderDivider('pre-existing debt', unicode, styler));
|
|
637
898
|
lines.push('');
|
|
638
|
-
lines.push(...
|
|
899
|
+
lines.push(...debt);
|
|
639
900
|
}
|
|
640
901
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
902
|
+
return lines;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Total width of a section divider, in columns.
|
|
907
|
+
*
|
|
908
|
+
* Chosen to sit inside an 80-column terminal alongside the two-space indent.
|
|
909
|
+
*
|
|
910
|
+
* @type {number}
|
|
911
|
+
*/
|
|
912
|
+
const DIVIDER_WIDTH = 54;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Render a labelled section divider.
|
|
916
|
+
*
|
|
917
|
+
* Falls back to ASCII dashes where box-drawing characters would not render, on
|
|
918
|
+
* the same gate as the wordmark and the ready panel.
|
|
919
|
+
*
|
|
920
|
+
* @param {string} label - Section label.
|
|
921
|
+
* @param {boolean} unicode - Whether box-drawing characters are safe to emit.
|
|
922
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
923
|
+
* @returns {string} Divider line.
|
|
924
|
+
*/
|
|
925
|
+
function renderDivider(label, unicode, styler) {
|
|
926
|
+
const rule = unicode ? '─' : '-';
|
|
927
|
+
const prefix = `${rule.repeat(2)} ${label} `;
|
|
928
|
+
const fill = Math.max(3, DIVIDER_WIDTH - prefix.length);
|
|
929
|
+
|
|
930
|
+
return `${INDENT}${styler('gray', `${prefix}${rule.repeat(fill)}`)}`;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Column headings for the verbose input listing.
|
|
935
|
+
*
|
|
936
|
+
* @type {{source: string, size: string}}
|
|
937
|
+
*/
|
|
938
|
+
const INPUT_FILE_HEADINGS = { source: 'source', size: 'size' };
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Column headings for the verbose output listing.
|
|
942
|
+
*
|
|
943
|
+
* @type {{file: string, size: string, gzip: string}}
|
|
944
|
+
*/
|
|
945
|
+
const OUTPUT_FILE_HEADINGS = { file: 'file', size: 'size', gzip: 'gzip' };
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Placeholder for a size that does not apply or could not be read.
|
|
949
|
+
*
|
|
950
|
+
* @type {string}
|
|
951
|
+
*/
|
|
952
|
+
const NO_SIZE = '—';
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Render a right-aligned size column.
|
|
956
|
+
*
|
|
957
|
+
* Padding is applied to the unstyled text because ANSI escapes carry no display
|
|
958
|
+
* width and would skew every row by a different amount.
|
|
959
|
+
*
|
|
960
|
+
* @param {number|undefined} bytes - Size in bytes.
|
|
961
|
+
* @param {number} width - Column width.
|
|
962
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
963
|
+
* @returns {string} Padded, styled size.
|
|
964
|
+
*/
|
|
965
|
+
const sizeColumn = (bytes, width, styler) =>
|
|
966
|
+
styler(
|
|
967
|
+
'gray',
|
|
968
|
+
(Number.isFinite(bytes) ? formatPreciseBytes(bytes) : NO_SIZE).padStart(
|
|
969
|
+
width,
|
|
970
|
+
),
|
|
646
971
|
);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Measure the widest rendered size in a set of rows.
|
|
975
|
+
*
|
|
976
|
+
* @param {Array<number|undefined>} values - Byte values.
|
|
977
|
+
* @param {string} heading - Column heading, which also has to fit.
|
|
978
|
+
* @returns {number} Column width.
|
|
979
|
+
*/
|
|
980
|
+
const sizeWidth = (values, heading) =>
|
|
981
|
+
Math.max(
|
|
982
|
+
heading.length,
|
|
983
|
+
...values.map(
|
|
984
|
+
(bytes) =>
|
|
985
|
+
(Number.isFinite(bytes) ? formatPreciseBytes(bytes) : NO_SIZE).length,
|
|
986
|
+
),
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Render the verbose listing of every entry the build reads.
|
|
991
|
+
*
|
|
992
|
+
* @param {Array<{path: string, bytes?: number}>} rows - Input file rows.
|
|
993
|
+
* @param {boolean} unicode - Whether box-drawing characters are safe to emit.
|
|
994
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
995
|
+
* @returns {string[]} Input listing lines.
|
|
996
|
+
*/
|
|
997
|
+
function renderInputFiles(rows, unicode, styler) {
|
|
998
|
+
if (rows.length === 0) return [];
|
|
999
|
+
|
|
1000
|
+
const pathWidth = Math.max(
|
|
1001
|
+
INPUT_FILE_HEADINGS.source.length,
|
|
1002
|
+
...rows.map((row) => row.path.length),
|
|
1003
|
+
);
|
|
1004
|
+
const width = sizeWidth(
|
|
1005
|
+
rows.map((row) => row.bytes),
|
|
1006
|
+
INPUT_FILE_HEADINGS.size,
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
const lines = [
|
|
1010
|
+
'',
|
|
1011
|
+
renderDivider('input files', unicode, styler),
|
|
1012
|
+
'',
|
|
1013
|
+
`${DETAIL_INDENT}${styler(
|
|
1014
|
+
'gray',
|
|
1015
|
+
`${INPUT_FILE_HEADINGS.source.padEnd(pathWidth)} ${INPUT_FILE_HEADINGS.size.padStart(width)}`,
|
|
1016
|
+
)}`,
|
|
1017
|
+
];
|
|
1018
|
+
|
|
1019
|
+
for (const row of rows) {
|
|
1020
|
+
lines.push(
|
|
1021
|
+
`${DETAIL_INDENT}${styler('cyan', row.path.padEnd(pathWidth))} ${sizeColumn(row.bytes, width, styler)}`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return lines;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Render the verbose listing of every file the build wrote.
|
|
1030
|
+
*
|
|
1031
|
+
* @param {Array<{fileName: string, bytes: number, gzipBytes?: number}>} rows - Output file rows.
|
|
1032
|
+
* @param {boolean} unicode - Whether box-drawing characters are safe to emit.
|
|
1033
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
1034
|
+
* @returns {string[]} Output listing lines.
|
|
1035
|
+
*/
|
|
1036
|
+
function renderOutputFiles(rows, unicode, styler) {
|
|
1037
|
+
if (rows.length === 0) return [];
|
|
1038
|
+
|
|
1039
|
+
const lines = [
|
|
1040
|
+
'',
|
|
1041
|
+
renderDivider('output files', unicode, styler),
|
|
1042
|
+
'',
|
|
1043
|
+
...renderSizeTable(rows, styler),
|
|
1044
|
+
];
|
|
1045
|
+
|
|
1046
|
+
return lines;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Render a file-and-size table, with a gzip column when any row has one.
|
|
1051
|
+
*
|
|
1052
|
+
* Shared by the first build's output listing and the rebuild's changed-file
|
|
1053
|
+
* listing so the two read identically — the second is a filtered view of the
|
|
1054
|
+
* first, and formatting them differently would obscure that.
|
|
1055
|
+
*
|
|
1056
|
+
* @param {Array<{fileName: string, bytes: number, gzipBytes?: number}>} rows - Output file rows.
|
|
1057
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
1058
|
+
* @returns {string[]} Table lines.
|
|
1059
|
+
*/
|
|
1060
|
+
function renderSizeTable(rows, styler) {
|
|
1061
|
+
const nameWidth = Math.max(
|
|
1062
|
+
OUTPUT_FILE_HEADINGS.file.length,
|
|
1063
|
+
...rows.map((row) => row.fileName.length),
|
|
1064
|
+
);
|
|
1065
|
+
const width = sizeWidth(
|
|
1066
|
+
rows.map((row) => row.bytes),
|
|
1067
|
+
OUTPUT_FILE_HEADINGS.size,
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
// The gzip column is dropped entirely when nothing in the table is
|
|
1071
|
+
// compressible, rather than printed as a column of dashes.
|
|
1072
|
+
const compressed = rows.some((row) => Number.isFinite(row.gzipBytes));
|
|
1073
|
+
const gzipHeading = compressed
|
|
1074
|
+
? ` ${OUTPUT_FILE_HEADINGS.gzip.padStart(
|
|
1075
|
+
sizeWidth(
|
|
1076
|
+
rows.map((row) => row.gzipBytes),
|
|
1077
|
+
OUTPUT_FILE_HEADINGS.gzip,
|
|
1078
|
+
),
|
|
1079
|
+
)}`
|
|
1080
|
+
: '';
|
|
1081
|
+
const gzipWidth = compressed
|
|
1082
|
+
? sizeWidth(
|
|
1083
|
+
rows.map((row) => row.gzipBytes),
|
|
1084
|
+
OUTPUT_FILE_HEADINGS.gzip,
|
|
1085
|
+
)
|
|
1086
|
+
: 0;
|
|
1087
|
+
|
|
1088
|
+
const lines = [
|
|
1089
|
+
`${DETAIL_INDENT}${styler(
|
|
1090
|
+
'gray',
|
|
1091
|
+
`${OUTPUT_FILE_HEADINGS.file.padEnd(nameWidth)} ${OUTPUT_FILE_HEADINGS.size.padStart(width)}${gzipHeading}`,
|
|
1092
|
+
)}`,
|
|
1093
|
+
];
|
|
1094
|
+
|
|
1095
|
+
for (const row of rows) {
|
|
1096
|
+
const gzip = compressed
|
|
1097
|
+
? ` ${sizeColumn(row.gzipBytes, gzipWidth, styler)}`
|
|
1098
|
+
: '';
|
|
1099
|
+
|
|
1100
|
+
lines.push(
|
|
1101
|
+
`${DETAIL_INDENT}${styler('cyan', row.fileName.padEnd(nameWidth))} ${sizeColumn(row.bytes, width, styler)}${gzip}`,
|
|
1102
|
+
);
|
|
650
1103
|
}
|
|
651
1104
|
|
|
652
1105
|
return lines;
|
|
@@ -655,6 +1108,10 @@ function renderProblems(
|
|
|
655
1108
|
/**
|
|
656
1109
|
* Render the summary printed after the first successful watch build.
|
|
657
1110
|
*
|
|
1111
|
+
* Emitted as four labelled sections — project, build, and whichever problem
|
|
1112
|
+
* headings have content. `watchLabel` is supplied by the plugin, which has the
|
|
1113
|
+
* resolved source roots; without it the label is inferred from the input rows.
|
|
1114
|
+
*
|
|
658
1115
|
* @param {{
|
|
659
1116
|
* snapshot: object,
|
|
660
1117
|
* durationMs: number,
|
|
@@ -663,6 +1120,13 @@ function renderProblems(
|
|
|
663
1120
|
* sourceGlob?: string,
|
|
664
1121
|
* assetRows?: Array<object>,
|
|
665
1122
|
* importErrors?: {rows?: Array<object>, sharedDirectory?: string, directoryExists?: boolean},
|
|
1123
|
+
* platform?: string,
|
|
1124
|
+
* inputRows?: Array<{name: string, path: string, count: number, overflow?: boolean}>,
|
|
1125
|
+
* watchLabel?: string,
|
|
1126
|
+
* write?: {fileCount: number, totalBytes: number, largest?: {fileName: string, bytes: number}},
|
|
1127
|
+
* inputFiles?: Array<{path: string, bytes?: number}>,
|
|
1128
|
+
* outputFiles?: Array<{fileName: string, bytes: number, gzipBytes?: number}>,
|
|
1129
|
+
* unicode?: boolean,
|
|
666
1130
|
* styler: (format: string|string[], text: string) => string
|
|
667
1131
|
* }} options - Summary inputs.
|
|
668
1132
|
* @returns {string[]} Summary lines.
|
|
@@ -676,6 +1140,13 @@ export function renderSummary({
|
|
|
676
1140
|
assetRows = [],
|
|
677
1141
|
importErrors = {},
|
|
678
1142
|
syntaxErrors = [],
|
|
1143
|
+
platform,
|
|
1144
|
+
inputRows = [],
|
|
1145
|
+
watchLabel,
|
|
1146
|
+
write,
|
|
1147
|
+
inputFiles = [],
|
|
1148
|
+
outputFiles = [],
|
|
1149
|
+
unicode = true,
|
|
679
1150
|
styler,
|
|
680
1151
|
}) {
|
|
681
1152
|
const failed =
|
|
@@ -689,8 +1160,36 @@ export function renderSummary({
|
|
|
689
1160
|
? `build failed after ${formatDuration(durationMs)}`
|
|
690
1161
|
: `built in ${formatDuration(durationMs)}`;
|
|
691
1162
|
|
|
692
|
-
|
|
693
|
-
|
|
1163
|
+
// `dist/` is written, not watched. Falling back to the input rows keeps the
|
|
1164
|
+
// label honest for any caller that renders a summary without the resolved
|
|
1165
|
+
// source roots to hand.
|
|
1166
|
+
const watching =
|
|
1167
|
+
watchLabel ||
|
|
1168
|
+
sharedRootPath(
|
|
1169
|
+
inputRows.filter((entry) => !entry.overflow).map((entry) => entry.path),
|
|
1170
|
+
) ||
|
|
1171
|
+
'sources';
|
|
1172
|
+
|
|
1173
|
+
// The two halves are labelled with the same dividers the problem blocks use, so
|
|
1174
|
+
// the whole summary reads as one sequence of named sections rather than a wall
|
|
1175
|
+
// of rows followed by some headings. The labels also give the facts block
|
|
1176
|
+
// somewhere to end: without one, `output` ran straight into the build result.
|
|
1177
|
+
//
|
|
1178
|
+
// Storybook's startup lines land between the banner and this block, so it opens
|
|
1179
|
+
// with a blank line rather than trusting whatever printed last to have left one.
|
|
1180
|
+
return [
|
|
1181
|
+
'',
|
|
1182
|
+
renderDivider('project', unicode, styler),
|
|
1183
|
+
'',
|
|
1184
|
+
...renderFacts({ platform, inputRows, outDir, write, styler }),
|
|
1185
|
+
// The verbose listings expand the two rows above them, so they sit directly
|
|
1186
|
+
// under the totals they itemize rather than after the build result.
|
|
1187
|
+
...renderInputFiles(inputFiles, unicode, styler),
|
|
1188
|
+
...renderOutputFiles(outputFiles, unicode, styler),
|
|
1189
|
+
'',
|
|
1190
|
+
renderDivider('build', unicode, styler),
|
|
1191
|
+
'',
|
|
1192
|
+
`${INDENT}${symbol} ${headline}${styler('gray', `${SEPARATOR}watching ${watching}`)}`,
|
|
694
1193
|
...renderProblems(
|
|
695
1194
|
snapshot,
|
|
696
1195
|
projectDir,
|
|
@@ -699,21 +1198,31 @@ export function renderSummary({
|
|
|
699
1198
|
assetRows,
|
|
700
1199
|
importErrors,
|
|
701
1200
|
syntaxErrors,
|
|
1201
|
+
unicode,
|
|
702
1202
|
),
|
|
703
1203
|
'',
|
|
704
1204
|
];
|
|
705
|
-
|
|
706
|
-
return lines;
|
|
707
1205
|
}
|
|
708
1206
|
|
|
709
1207
|
/**
|
|
710
1208
|
* Render the compact line printed after each watch rebuild.
|
|
711
1209
|
*
|
|
1210
|
+
* In detailed mode the line is followed by what the rebuild actually produced:
|
|
1211
|
+
* how many modules were transformed, and which outputs came out different. That
|
|
1212
|
+
* is a deliberate departure from Rolldown's table, which reprints all seventy-odd
|
|
1213
|
+
* files every cycle because Rollup regenerates the whole bundle every cycle. The
|
|
1214
|
+
* question after an edit is which files changed, and the negative answer — an
|
|
1215
|
+
* edit that compiled to byte-identical output — is worth a line of its own.
|
|
1216
|
+
*
|
|
712
1217
|
* @param {{
|
|
713
1218
|
* snapshot: object,
|
|
714
1219
|
* durationMs: number,
|
|
715
1220
|
* changedFiles?: string[],
|
|
716
1221
|
* projectDir?: string,
|
|
1222
|
+
* moduleCount?: number,
|
|
1223
|
+
* changedOutputs?: Array<{fileName: string, bytes: number, gzipBytes?: number}>,
|
|
1224
|
+
* removedOutputs?: string[],
|
|
1225
|
+
* detailed?: boolean,
|
|
717
1226
|
* styler: (format: string|string[], text: string) => string,
|
|
718
1227
|
* now?: Date
|
|
719
1228
|
* }} options - Rebuild inputs.
|
|
@@ -724,6 +1233,10 @@ export function renderRebuild({
|
|
|
724
1233
|
durationMs,
|
|
725
1234
|
changedFiles = [],
|
|
726
1235
|
projectDir = '',
|
|
1236
|
+
moduleCount,
|
|
1237
|
+
changedOutputs = [],
|
|
1238
|
+
removedOutputs = [],
|
|
1239
|
+
detailed = false,
|
|
727
1240
|
styler,
|
|
728
1241
|
now = new Date(),
|
|
729
1242
|
}) {
|
|
@@ -752,6 +1265,67 @@ export function renderRebuild({
|
|
|
752
1265
|
// this reporter exists to remove, so rebuilds only surface hard failures.
|
|
753
1266
|
if (failed) {
|
|
754
1267
|
lines.push(...renderDetailRows(snapshot.errors, projectDir, styler));
|
|
1268
|
+
return lines;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (detailed)
|
|
1272
|
+
lines.push(
|
|
1273
|
+
...renderRebuildDetail(
|
|
1274
|
+
{ moduleCount, changedOutputs, removedOutputs },
|
|
1275
|
+
styler,
|
|
1276
|
+
),
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
return lines;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Render the detailed tail of a successful rebuild.
|
|
1284
|
+
*
|
|
1285
|
+
* @param {{
|
|
1286
|
+
* moduleCount?: number,
|
|
1287
|
+
* changedOutputs?: Array<{fileName: string, bytes: number, gzipBytes?: number}>,
|
|
1288
|
+
* removedOutputs?: string[]
|
|
1289
|
+
* }} cycle - What the rebuild produced.
|
|
1290
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
1291
|
+
* @returns {string[]} Detail lines.
|
|
1292
|
+
*/
|
|
1293
|
+
function renderRebuildDetail(
|
|
1294
|
+
{ moduleCount, changedOutputs = [], removedOutputs = [] },
|
|
1295
|
+
styler,
|
|
1296
|
+
) {
|
|
1297
|
+
const facts = [];
|
|
1298
|
+
if (Number.isFinite(moduleCount)) {
|
|
1299
|
+
facts.push(`${pluralize(moduleCount, 'module')} transformed`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
facts.push(
|
|
1303
|
+
changedOutputs.length === 0
|
|
1304
|
+
? 'no output changed'
|
|
1305
|
+
: `${pluralize(changedOutputs.length, 'output')} changed`,
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
if (removedOutputs.length > 0) {
|
|
1309
|
+
facts.push(`${pluralize(removedOutputs.length, 'output')} removed`);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const lines = [
|
|
1313
|
+
'',
|
|
1314
|
+
`${DETAIL_INDENT}${styler('gray', facts.join(SEPARATOR))}`,
|
|
1315
|
+
];
|
|
1316
|
+
|
|
1317
|
+
if (changedOutputs.length > 0) {
|
|
1318
|
+
lines.push('', ...renderSizeTable(changedOutputs, styler));
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Removals carry no size, so they cannot share the table without a column of
|
|
1322
|
+
// dashes. They get their own labelled group instead.
|
|
1323
|
+
if (removedOutputs.length > 0) {
|
|
1324
|
+
lines.push('', `${DETAIL_INDENT}${styler('gray', 'no longer written')}`);
|
|
1325
|
+
|
|
1326
|
+
for (const fileName of removedOutputs) {
|
|
1327
|
+
lines.push(`${DETAIL_INDENT}${styler('cyan', fileName)}`);
|
|
1328
|
+
}
|
|
755
1329
|
}
|
|
756
1330
|
|
|
757
1331
|
return lines;
|