@emulsify/core 4.2.0 → 4.3.0
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 +91 -36
- package/README.md +185 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
- package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
- package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
- package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
- package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
- package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
- package/config/vite/plugins/index.js +23 -14
- package/config/vite/plugins/reporter/asset-resolver.js +541 -0
- package/config/vite/plugins/reporter/build-errors.js +284 -0
- package/config/vite/plugins/reporter/diagnostics.js +366 -0
- package/config/vite/plugins/reporter/format.js +199 -0
- package/config/vite/plugins/reporter/index.js +308 -0
- package/config/vite/plugins/reporter/render.js +758 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/vite-logger.js +188 -0
- package/config/vite/plugins/reporter/watch-mode.js +40 -0
- package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
- package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
- package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +387 -115
- package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
- package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
- package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
- package/config/vite/plugins.js +1 -1
- package/config/vite/project-config.js +1 -1
- package/config/vite/project-structure.js +1 -1
- package/config/vite/utils/lru.js +77 -0
- package/config/vite/utils/package-version.js +42 -0
- package/config/vite/utils/paths.js +1 -9
- package/config/vite/utils/react-singleton.js +1 -1
- package/config/vite/vite.config.js +51 -5
- package/package.json +93 -58
- package/scripts/a11y.js +115 -23
- package/scripts/audit/checks/core-imports.js +78 -0
- package/scripts/audit/checks/css-asset-references.js +99 -0
- package/scripts/audit/checks/drupal-assumptions.js +48 -0
- package/scripts/audit/checks/files-outside-roots.js +53 -0
- package/scripts/audit/checks/generated-package-scripts.js +113 -0
- package/scripts/audit/checks/legacy-twig-stories.js +33 -0
- package/scripts/audit/checks/package-overrides.js +91 -0
- package/scripts/audit/checks/project-config.js +71 -0
- package/scripts/audit/checks/story-discovery.js +35 -0
- package/scripts/audit/checks/twig-references.js +69 -0
- package/scripts/audit/checks/twig-volume.js +54 -0
- package/scripts/audit/checks/webpack-patterns.js +86 -0
- package/scripts/audit/index.js +177 -0
- package/scripts/audit/lib/css.js +165 -0
- package/scripts/audit/lib/files.js +168 -0
- package/scripts/audit/lib/findings.js +31 -0
- package/scripts/audit/lib/package-json.js +65 -0
- package/scripts/audit/lib/twig.js +227 -0
- package/scripts/audit/report.js +273 -0
- package/scripts/audit-twig-stories.js +115 -78
- package/scripts/audit.js +150 -1632
- package/scripts/check-node-version.js +136 -10
- package/scripts/inspect-components.js +456 -0
- package/scripts/lib/cli.js +179 -0
- package/scripts/lib/fs.js +31 -0
- package/scripts/lib/proc.js +78 -0
- package/scripts/lib/text.js +14 -0
- package/scripts/loadYaml.js +2 -2
- package/src/extensions/shared/attributes.js +3 -3
- package/src/extensions/shared/lists.js +2 -6
- package/src/extensions/shared/root-relative.js +38 -0
- package/src/storybook/index.js +4 -0
- package/src/storybook/render-twig.js +1 -1
- package/src/storybook/render-web-component.js +459 -0
- package/src/storybook/twig/asset-source-runtime.js +193 -0
- package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
- package/src/storybook/twig/reference-paths.js +2 -13
- package/src/storybook/twig/resolver.js +20 -3
- package/src/storybook/twig/setup.js +12 -2
- package/src/storybook/twig/source-function.js +5 -2
- package/config/vite/utils/unique.js +0 -36
- package/src/storybook/twig/include.js +0 -28
- package/src/storybook/twig/source-events.js +0 -5
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Line rendering for the Emulsify develop reporter.
|
|
3
|
+
*
|
|
4
|
+
* Every function here is pure: it takes a diagnostics snapshot and returns an
|
|
5
|
+
* array of finished lines. Keeping rendering separate from the Vite plugin
|
|
6
|
+
* lifecycle means the output can be asserted directly in tests without running
|
|
7
|
+
* a build, and it enforces the append-only constraint by construction — there
|
|
8
|
+
* is no stream to rewrite, only strings to hand back.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
SYMBOLS,
|
|
13
|
+
displayLocation,
|
|
14
|
+
displayPath,
|
|
15
|
+
formatClockTime,
|
|
16
|
+
formatDuration,
|
|
17
|
+
platformLabel,
|
|
18
|
+
pluralize,
|
|
19
|
+
} from './format.js';
|
|
20
|
+
import { deprecationFix, deprecationMigrator } from './sass-logger.js';
|
|
21
|
+
|
|
22
|
+
const INDENT = ' ';
|
|
23
|
+
const DETAIL_INDENT = ' ';
|
|
24
|
+
const ROW_INDENT = ' ';
|
|
25
|
+
const SEPARATOR = ' · ';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum number of individual problems listed before collapsing to a count.
|
|
29
|
+
*
|
|
30
|
+
* @type {number}
|
|
31
|
+
*/
|
|
32
|
+
const MAX_DETAIL_ROWS = 5;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of unresolved asset URLs listed before collapsing.
|
|
36
|
+
*
|
|
37
|
+
* Higher than the general detail cap because these rows are single short
|
|
38
|
+
* paths, and the same image referenced from two stylesheets with different
|
|
39
|
+
* relative spellings is two separate edits — hiding either is unhelpful.
|
|
40
|
+
*
|
|
41
|
+
* @type {number}
|
|
42
|
+
*/
|
|
43
|
+
const MAX_ASSET_ROWS = 8;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Maximum number of files listed in the deprecation worklist.
|
|
47
|
+
*
|
|
48
|
+
* @type {number}
|
|
49
|
+
*/
|
|
50
|
+
const MAX_DEPRECATION_FILES = 4;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum number of deprecation kinds listed beneath a single file.
|
|
54
|
+
*
|
|
55
|
+
* @type {number}
|
|
56
|
+
*/
|
|
57
|
+
const MAX_DEPRECATION_KINDS_PER_FILE = 4;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Maximum number of line numbers named on one row before collapsing.
|
|
61
|
+
*
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
const MAX_LINES_PER_ROW = 3;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render the affected line numbers for one deprecation within one file.
|
|
68
|
+
*
|
|
69
|
+
* @param {number[]} lineNumbers - Sorted line numbers.
|
|
70
|
+
* @returns {string} Compact line reference, for example `:30,31 +2`.
|
|
71
|
+
*/
|
|
72
|
+
export function formatLineList(lineNumbers = []) {
|
|
73
|
+
if (lineNumbers.length === 0) return ':?';
|
|
74
|
+
|
|
75
|
+
const shown = lineNumbers.slice(0, MAX_LINES_PER_ROW).join(',');
|
|
76
|
+
const hidden = lineNumbers.length - MAX_LINES_PER_ROW;
|
|
77
|
+
|
|
78
|
+
return hidden > 0 ? `:${shown} +${hidden}` : `:${shown}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Half-block wordmark drawn when the terminal can render the glyphs.
|
|
83
|
+
*
|
|
84
|
+
* Spells EMULSIFY at 31 columns, which fits comfortably in an 80-column
|
|
85
|
+
* terminal alongside the two-space indent.
|
|
86
|
+
*
|
|
87
|
+
* @type {string[]}
|
|
88
|
+
*/
|
|
89
|
+
const WORDMARK = [
|
|
90
|
+
'█▀▀ █▀▄▀█ █ █ █ █▀▀ █ █▀▀ █ █',
|
|
91
|
+
'█▀▀ █ ▀ █ █ █ █ ▀▀█ █ █▀▀ ▀▄▀',
|
|
92
|
+
'▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀ ',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render the header shown once when the watcher starts.
|
|
97
|
+
*
|
|
98
|
+
* The wordmark exists to mark where Emulsify's output begins. `npm run develop`
|
|
99
|
+
* interleaves npm's script echo, Vite, and Storybook, so a single dim line is
|
|
100
|
+
* easy to scroll past; a block of art is not. Terminals that cannot render the
|
|
101
|
+
* glyphs get the plain name instead of mojibake.
|
|
102
|
+
*
|
|
103
|
+
* @param {{
|
|
104
|
+
* version?: string,
|
|
105
|
+
* platform?: string,
|
|
106
|
+
* entryCount?: number,
|
|
107
|
+
* unicode?: boolean,
|
|
108
|
+
* styler: (format: string|string[], text: string) => string
|
|
109
|
+
* }} options - Banner inputs.
|
|
110
|
+
* @returns {string[]} Banner lines.
|
|
111
|
+
*/
|
|
112
|
+
export function renderBanner({
|
|
113
|
+
version,
|
|
114
|
+
platform,
|
|
115
|
+
entryCount,
|
|
116
|
+
unicode = true,
|
|
117
|
+
styler,
|
|
118
|
+
}) {
|
|
119
|
+
const facts = [
|
|
120
|
+
`core ${version || '0.0.0'}`,
|
|
121
|
+
`Platform: ${platformLabel(platform)}`,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
if (Number.isFinite(entryCount)) {
|
|
125
|
+
facts.push(pluralize(entryCount, 'entry', 'entries'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const mark = unicode
|
|
129
|
+
? WORDMARK.map((row) => `${INDENT}${styler(['bold', 'cyan'], row)}`)
|
|
130
|
+
: [`${INDENT}${styler(['bold', 'cyan'], 'EMULSIFY')}`];
|
|
131
|
+
|
|
132
|
+
return ['', ...mark, `${INDENT}${styler('gray', facts.join(SEPARATOR))}`, ''];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Render the detail rows for a set of errors or warnings.
|
|
137
|
+
*
|
|
138
|
+
* @param {Array<{message?: string, file?: string, line?: number, count: number}>} entries - Reported entries.
|
|
139
|
+
* @param {string} projectDir - Project root.
|
|
140
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
141
|
+
* @returns {string[]} Detail lines.
|
|
142
|
+
*/
|
|
143
|
+
function renderDetailRows(entries, projectDir, styler) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
|
|
146
|
+
for (const entry of entries.slice(0, MAX_DETAIL_ROWS)) {
|
|
147
|
+
const repeat = entry.count > 1 ? styler('gray', ` (×${entry.count})`) : '';
|
|
148
|
+
|
|
149
|
+
if (entry.file) {
|
|
150
|
+
const location = displayLocation(entry.file, entry.line, projectDir);
|
|
151
|
+
lines.push(`${DETAIL_INDENT}${styler('gray', location)}${repeat}`);
|
|
152
|
+
|
|
153
|
+
if (entry.message) {
|
|
154
|
+
lines.push(`${DETAIL_INDENT}${entry.message}`);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Some warnings arrive with no span. Printing a "<unknown>" path row for
|
|
160
|
+
// those wastes a line and reads like a failure to resolve something, so the
|
|
161
|
+
// message carries the repeat count instead. An entry with neither a
|
|
162
|
+
// location nor a message has nothing to act on and is dropped.
|
|
163
|
+
if (entry.message) {
|
|
164
|
+
lines.push(`${DETAIL_INDENT}${entry.message}${repeat}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const hidden = entries.length - MAX_DETAIL_ROWS;
|
|
169
|
+
if (hidden > 0) {
|
|
170
|
+
lines.push(
|
|
171
|
+
`${DETAIL_INDENT}${styler('gray', `+${pluralize(hidden, 'more')}`)}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Render the deduplicated Sass deprecation block.
|
|
180
|
+
*
|
|
181
|
+
* This replaces several hundred lines of repeated Dart Sass output with a
|
|
182
|
+
* worklist: total first, then each affected file with the deprecations inside
|
|
183
|
+
* it, then the command that fixes most of them. Each row carries the affected
|
|
184
|
+
* lines, how many occurrences, the Sass deprecation ID, and the substitution to
|
|
185
|
+
* make — the ID alone identifies nothing actionable, and the substitution alone
|
|
186
|
+
* gives no way to look up the details.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} snapshot - Diagnostics snapshot.
|
|
189
|
+
* @param {string} projectDir - Project root.
|
|
190
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
191
|
+
* @param {string} sourceGlob - Glob matching the project stylesheets.
|
|
192
|
+
* @returns {string[]} Deprecation summary lines.
|
|
193
|
+
*/
|
|
194
|
+
function renderDeprecations(snapshot, projectDir, styler, sourceGlob) {
|
|
195
|
+
const { deprecations, deprecationsByFile, deprecationTotal } = snapshot;
|
|
196
|
+
if (deprecations.length === 0) return [];
|
|
197
|
+
|
|
198
|
+
const headline = [
|
|
199
|
+
pluralize(deprecationTotal, 'sass deprecation'),
|
|
200
|
+
pluralize(deprecationsByFile.length || 1, 'file'),
|
|
201
|
+
].join(SEPARATOR);
|
|
202
|
+
|
|
203
|
+
const lines = [
|
|
204
|
+
`${INDENT}${styler('yellow', SYMBOLS.warning)} ${styler('yellow', headline)}`,
|
|
205
|
+
'',
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const shownFiles = deprecationsByFile.slice(0, MAX_DEPRECATION_FILES);
|
|
209
|
+
|
|
210
|
+
// Column widths are measured across every row that will be printed so the
|
|
211
|
+
// line, count, and ID columns align and the block scans as a table. Padding
|
|
212
|
+
// is applied before styling so escape sequences never affect the width.
|
|
213
|
+
const rows = shownFiles.flatMap((group) =>
|
|
214
|
+
group.entries.slice(0, MAX_DEPRECATION_KINDS_PER_FILE),
|
|
215
|
+
);
|
|
216
|
+
const lineWidth = Math.max(
|
|
217
|
+
DEPRECATION_HEADINGS.lines.length,
|
|
218
|
+
...rows.map((e) => formatLineList(e.lines).length),
|
|
219
|
+
);
|
|
220
|
+
const countWidth = Math.max(
|
|
221
|
+
DEPRECATION_HEADINGS.count.length,
|
|
222
|
+
...rows.map((e) => `${e.count}×`.length),
|
|
223
|
+
);
|
|
224
|
+
const idWidth = Math.max(
|
|
225
|
+
DEPRECATION_HEADINGS.id.length,
|
|
226
|
+
...rows.map((e) => e.id.length),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
lines.push(
|
|
230
|
+
`${ROW_INDENT}${styler(
|
|
231
|
+
'gray',
|
|
232
|
+
`${DEPRECATION_HEADINGS.lines.padEnd(lineWidth)} ${DEPRECATION_HEADINGS.count.padStart(countWidth)} ${DEPRECATION_HEADINGS.id.padEnd(idWidth)} ${DEPRECATION_HEADINGS.fix}`,
|
|
233
|
+
)}`,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
for (const group of shownFiles) {
|
|
237
|
+
lines.push(
|
|
238
|
+
`${DETAIL_INDENT}${styler('cyan', displayPath(group.file, projectDir))}`,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
for (const entry of group.entries.slice(
|
|
242
|
+
0,
|
|
243
|
+
MAX_DEPRECATION_KINDS_PER_FILE,
|
|
244
|
+
)) {
|
|
245
|
+
const lineRef = formatLineList(entry.lines).padEnd(lineWidth);
|
|
246
|
+
const count = `${entry.count}×`.padStart(countWidth);
|
|
247
|
+
const id = entry.id.padEnd(idWidth);
|
|
248
|
+
const fix = deprecationFix(entry.id) || '';
|
|
249
|
+
|
|
250
|
+
lines.push(
|
|
251
|
+
`${ROW_INDENT}${styler('gray', lineRef)} ${styler('yellow', count)} ${styler('gray', id)} ${fix}`.trimEnd(),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const hiddenKinds = group.entries.length - MAX_DEPRECATION_KINDS_PER_FILE;
|
|
256
|
+
if (hiddenKinds > 0) {
|
|
257
|
+
lines.push(
|
|
258
|
+
`${ROW_INDENT}${styler('gray', `+${pluralize(hiddenKinds, 'more kind')}`)}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const hiddenFiles = deprecationsByFile.length - MAX_DEPRECATION_FILES;
|
|
264
|
+
if (hiddenFiles > 0) {
|
|
265
|
+
lines.push(
|
|
266
|
+
`${DETAIL_INDENT}${styler('gray', `+${pluralize(hiddenFiles, 'more file')}`)}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const command = renderMigratorCommand(deprecations, sourceGlob, styler);
|
|
271
|
+
if (command) {
|
|
272
|
+
lines.push('', command);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return lines;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Render the `sass-migrator` invocation that resolves most of the debt.
|
|
280
|
+
*
|
|
281
|
+
* The migrator runs exactly one migration per invocation, so a combined command
|
|
282
|
+
* would not work. The dominant migration is shown in full and any others are
|
|
283
|
+
* named after it, which keeps the block to one line while staying accurate
|
|
284
|
+
* about what has to be run.
|
|
285
|
+
*
|
|
286
|
+
* @param {Array<{id: string, occurrences: number}>} deprecations - ID-keyed buckets, most frequent first.
|
|
287
|
+
* @param {string} sourceGlob - Glob matching the project stylesheets.
|
|
288
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
289
|
+
* @returns {string|undefined} Command line, when a migrator applies.
|
|
290
|
+
* @see https://sass-lang.com/documentation/cli/migrator/
|
|
291
|
+
*/
|
|
292
|
+
function renderMigratorCommand(deprecations, sourceGlob, styler) {
|
|
293
|
+
const migrators = [];
|
|
294
|
+
for (const bucket of deprecations) {
|
|
295
|
+
const migrator = deprecationMigrator(bucket.id);
|
|
296
|
+
if (migrator && !migrators.includes(migrator)) migrators.push(migrator);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (migrators.length === 0) return undefined;
|
|
300
|
+
|
|
301
|
+
const [primary, ...rest] = migrators;
|
|
302
|
+
const command = `npx sass-migrator ${primary} '${sourceGlob}'`;
|
|
303
|
+
const others = rest.length > 0 ? ` (then: ${rest.join(', ')})` : '';
|
|
304
|
+
|
|
305
|
+
return `${DETAIL_INDENT}${styler('gray', command + others)}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Column headings for the unresolved asset table.
|
|
310
|
+
*
|
|
311
|
+
* `on disk` has to label both a directory and a not-found state, which is why
|
|
312
|
+
* it is not called something like "found in".
|
|
313
|
+
*
|
|
314
|
+
* @type {{where: string, url: string, disk: string}}
|
|
315
|
+
*/
|
|
316
|
+
const ASSET_HEADINGS = {
|
|
317
|
+
where: 'referenced in',
|
|
318
|
+
url: 'url',
|
|
319
|
+
disk: 'on disk',
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Column headings for the deprecation worklist.
|
|
324
|
+
*
|
|
325
|
+
* These sit at the row indent rather than the file indent so each label lands
|
|
326
|
+
* directly above the column it names.
|
|
327
|
+
*
|
|
328
|
+
* @type {{lines: string, count: string, id: string, fix: string}}
|
|
329
|
+
*/
|
|
330
|
+
const DEPRECATION_HEADINGS = {
|
|
331
|
+
lines: 'lines',
|
|
332
|
+
count: 'count',
|
|
333
|
+
id: 'deprecation',
|
|
334
|
+
fix: 'fix',
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Colors for each on-disk resolution state.
|
|
339
|
+
*
|
|
340
|
+
* @type {Record<string, string>}
|
|
341
|
+
*/
|
|
342
|
+
const ASSET_STATUS_COLORS = {
|
|
343
|
+
found: 'green',
|
|
344
|
+
missing: 'red',
|
|
345
|
+
ambiguous: 'yellow',
|
|
346
|
+
unknown: 'gray',
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Column headings for the missing-import table.
|
|
351
|
+
*
|
|
352
|
+
* @type {{where: string, specifier: string, disk: string}}
|
|
353
|
+
*/
|
|
354
|
+
const IMPORT_HEADINGS = {
|
|
355
|
+
where: 'imported by',
|
|
356
|
+
specifier: 'import',
|
|
357
|
+
disk: 'on disk',
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Maximum likely-source leads listed for one syntax error.
|
|
362
|
+
*
|
|
363
|
+
* @type {number}
|
|
364
|
+
*/
|
|
365
|
+
const MAX_SOURCE_LEADS = 4;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Render the CSS syntax error block.
|
|
369
|
+
*
|
|
370
|
+
* The minifier runs on the concatenated bundle, so its line number refers to
|
|
371
|
+
* generated CSS and names no file. The offending declaration is shown verbatim
|
|
372
|
+
* with the minifier's caret, followed by wherever that declaration's literals
|
|
373
|
+
* appear in the project. Those are labelled likely, not definite, because they
|
|
374
|
+
* come from a search rather than a source map.
|
|
375
|
+
*
|
|
376
|
+
* @param {Array<{
|
|
377
|
+
* minifier: string,
|
|
378
|
+
* message: string,
|
|
379
|
+
* bundleLine?: number,
|
|
380
|
+
* declaration?: string,
|
|
381
|
+
* caretColumn?: number,
|
|
382
|
+
* lead?: {token: string, matches: Array<{file: string, line: number}>}
|
|
383
|
+
* }>} errors - Parsed syntax errors.
|
|
384
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
385
|
+
* @returns {string[]} Syntax error lines.
|
|
386
|
+
*/
|
|
387
|
+
function renderSyntaxErrors(errors, styler) {
|
|
388
|
+
if (errors.length === 0) return [];
|
|
389
|
+
|
|
390
|
+
const lines = [
|
|
391
|
+
`${INDENT}${styler('red', SYMBOLS.error)} ${styler('red', pluralize(errors.length, 'css syntax error'))}`,
|
|
392
|
+
'',
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
for (const error of errors) {
|
|
396
|
+
lines.push(
|
|
397
|
+
`${DETAIL_INDENT}${styler('gray', error.minifier)} ${error.message}`,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (error.declaration) {
|
|
401
|
+
lines.push(`${DETAIL_INDENT}${error.declaration}`);
|
|
402
|
+
|
|
403
|
+
if (error.caretColumn != null) {
|
|
404
|
+
lines.push(
|
|
405
|
+
`${DETAIL_INDENT}${' '.repeat(error.caretColumn)}${styler('red', '^')}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const matches = error.lead?.matches || [];
|
|
411
|
+
if (matches.length > 0) {
|
|
412
|
+
lines.push('', `${DETAIL_INDENT}${styler('gray', 'likely source')}`);
|
|
413
|
+
|
|
414
|
+
const width = Math.max(
|
|
415
|
+
...matches
|
|
416
|
+
.slice(0, MAX_SOURCE_LEADS)
|
|
417
|
+
.map((match) => `${match.file}:${match.line}`.length),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
for (const match of matches.slice(0, MAX_SOURCE_LEADS)) {
|
|
421
|
+
const location = `${match.file}:${match.line}`.padEnd(width);
|
|
422
|
+
lines.push(
|
|
423
|
+
`${DETAIL_INDENT}${styler('cyan', location)} ${styler('gray', error.lead.token)}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const hidden = matches.length - MAX_SOURCE_LEADS;
|
|
428
|
+
if (hidden > 0) {
|
|
429
|
+
lines.push(
|
|
430
|
+
`${DETAIL_INDENT}${styler('gray', `+${pluralize(hidden, 'more')}`)}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (error.bundleLine != null) {
|
|
436
|
+
// Named as generated output so it never reads as a source location.
|
|
437
|
+
lines.push(
|
|
438
|
+
'',
|
|
439
|
+
`${DETAIL_INDENT}${styler('gray', `bundle line ${error.bundleLine} · EMULSIFY_VERBOSE=1 for full output`)}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return lines;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Render the missing Sass import block.
|
|
449
|
+
*
|
|
450
|
+
* Replaces Rolldown's aggregate dump, which prints every error three times
|
|
451
|
+
* with a Dart Sass stack trace attached. The table keeps one row per importing
|
|
452
|
+
* site and closes with the directory they all point into, because a deleted
|
|
453
|
+
* partial usually breaks several components at once.
|
|
454
|
+
*
|
|
455
|
+
* @param {Array<{where: string, specifier: string, status: string, label: string}>} rows - Import rows.
|
|
456
|
+
* @param {string|undefined} sharedDirectory - Directory every import resolves under.
|
|
457
|
+
* @param {boolean} directoryExists - Whether that directory is present.
|
|
458
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
459
|
+
* @returns {string[]} Missing import lines.
|
|
460
|
+
*/
|
|
461
|
+
function renderImportErrors(rows, sharedDirectory, directoryExists, styler) {
|
|
462
|
+
if (rows.length === 0) return [];
|
|
463
|
+
|
|
464
|
+
const shown = rows.slice(0, MAX_ASSET_ROWS);
|
|
465
|
+
const distinct = new Set(rows.map((row) => row.specifier)).size;
|
|
466
|
+
const headline = [
|
|
467
|
+
pluralize(distinct, 'missing stylesheet'),
|
|
468
|
+
pluralize(rows.length, 'import error'),
|
|
469
|
+
].join(SEPARATOR);
|
|
470
|
+
|
|
471
|
+
const whereWidth = Math.max(
|
|
472
|
+
IMPORT_HEADINGS.where.length,
|
|
473
|
+
...shown.map((row) => row.where.length),
|
|
474
|
+
);
|
|
475
|
+
const specifierWidth = Math.max(
|
|
476
|
+
IMPORT_HEADINGS.specifier.length,
|
|
477
|
+
...shown.map((row) => row.specifier.length),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const lines = [
|
|
481
|
+
`${INDENT}${styler('red', SYMBOLS.error)} ${styler('red', headline)}`,
|
|
482
|
+
'',
|
|
483
|
+
`${DETAIL_INDENT}${styler(
|
|
484
|
+
'gray',
|
|
485
|
+
`${IMPORT_HEADINGS.where.padEnd(whereWidth)} ${IMPORT_HEADINGS.specifier.padEnd(specifierWidth)} ${IMPORT_HEADINGS.disk}`,
|
|
486
|
+
)}`,
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
for (const row of shown) {
|
|
490
|
+
const where = styler('cyan', row.where.padEnd(whereWidth));
|
|
491
|
+
const specifier = row.specifier.padEnd(specifierWidth);
|
|
492
|
+
const disk = styler(row.status === 'moved' ? 'yellow' : 'red', row.label);
|
|
493
|
+
|
|
494
|
+
lines.push(`${DETAIL_INDENT}${where} ${specifier} ${disk}`.trimEnd());
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const hidden = rows.length - MAX_ASSET_ROWS;
|
|
498
|
+
if (hidden > 0) {
|
|
499
|
+
lines.push(
|
|
500
|
+
`${DETAIL_INDENT}${styler('gray', `+${pluralize(hidden, 'more')}`)}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (sharedDirectory) {
|
|
505
|
+
const cause = directoryExists
|
|
506
|
+
? `all ${rows.length} resolve under ${sharedDirectory}`
|
|
507
|
+
: `all ${rows.length} resolve under ${sharedDirectory} — directory not found`;
|
|
508
|
+
lines.push('', `${DETAIL_INDENT}${styler('gray', cause)}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return lines;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Render the unresolved CSS asset block.
|
|
516
|
+
*
|
|
517
|
+
* Vite prints one of these per `url()` it cannot resolve, mid-build and in
|
|
518
|
+
* whatever order the transforms finish. Collapsing them into one table also
|
|
519
|
+
* lets the reporter answer what the raw notice cannot: which stylesheet writes
|
|
520
|
+
* the URL, and whether the file exists anywhere in the project.
|
|
521
|
+
*
|
|
522
|
+
* @param {Array<{where: string, url: string, status: string, label: string}>} rows - Enriched rows.
|
|
523
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
524
|
+
* @returns {string[]} Unresolved asset lines.
|
|
525
|
+
*/
|
|
526
|
+
function renderUnresolvedAssets(rows, styler) {
|
|
527
|
+
if (rows.length === 0) return [];
|
|
528
|
+
|
|
529
|
+
const shown = rows.slice(0, MAX_ASSET_ROWS);
|
|
530
|
+
const found = rows.filter((row) => row.status === 'found').length;
|
|
531
|
+
const missing = rows.filter((row) => row.status === 'missing').length;
|
|
532
|
+
|
|
533
|
+
const tally = [`${found} found`, `${missing} missing`].join(', ');
|
|
534
|
+
const headline = `${pluralize(rows.length, 'unresolved css url')}${SEPARATOR}${tally}`;
|
|
535
|
+
|
|
536
|
+
// Padding is applied before styling so ANSI escapes never skew the columns.
|
|
537
|
+
const whereWidth = Math.max(
|
|
538
|
+
ASSET_HEADINGS.where.length,
|
|
539
|
+
...shown.map((row) => row.where.length),
|
|
540
|
+
);
|
|
541
|
+
const urlWidth = Math.max(
|
|
542
|
+
ASSET_HEADINGS.url.length,
|
|
543
|
+
...shown.map((row) => row.url.length),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const lines = [
|
|
547
|
+
`${INDENT}${styler('yellow', SYMBOLS.warning)} ${styler('yellow', headline)}`,
|
|
548
|
+
'',
|
|
549
|
+
`${DETAIL_INDENT}${styler(
|
|
550
|
+
'gray',
|
|
551
|
+
`${ASSET_HEADINGS.where.padEnd(whereWidth)} ${ASSET_HEADINGS.url.padEnd(urlWidth)} ${ASSET_HEADINGS.disk}`,
|
|
552
|
+
)}`,
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
for (const row of shown) {
|
|
556
|
+
const where = styler('cyan', row.where.padEnd(whereWidth));
|
|
557
|
+
const url = row.url.padEnd(urlWidth);
|
|
558
|
+
const disk = styler(ASSET_STATUS_COLORS[row.status] || 'gray', row.label);
|
|
559
|
+
|
|
560
|
+
lines.push(`${DETAIL_INDENT}${where} ${url} ${disk}`.trimEnd());
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const hidden = rows.length - MAX_ASSET_ROWS;
|
|
564
|
+
if (hidden > 0) {
|
|
565
|
+
lines.push(
|
|
566
|
+
`${DETAIL_INDENT}${styler('gray', `+${pluralize(hidden, 'more')}`)}`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
lines.push(
|
|
571
|
+
'',
|
|
572
|
+
`${DETAIL_INDENT}${styler(
|
|
573
|
+
'gray',
|
|
574
|
+
'paths resolve from dist/, not from the scss file',
|
|
575
|
+
)}`,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
return lines;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Render the problem blocks shared by first builds and rebuilds.
|
|
583
|
+
*
|
|
584
|
+
* @param {object} snapshot - Diagnostics snapshot.
|
|
585
|
+
* @param {string} projectDir - Project root.
|
|
586
|
+
* @param {(format: string|string[], text: string) => string} styler - Styling function.
|
|
587
|
+
* @param {string} sourceGlob - Glob matching the project stylesheets.
|
|
588
|
+
* @param {Array<object>} assetRows - Enriched unresolved asset rows.
|
|
589
|
+
* @returns {string[]} Problem lines.
|
|
590
|
+
*/
|
|
591
|
+
function renderProblems(
|
|
592
|
+
snapshot,
|
|
593
|
+
projectDir,
|
|
594
|
+
styler,
|
|
595
|
+
sourceGlob,
|
|
596
|
+
assetRows,
|
|
597
|
+
importErrors,
|
|
598
|
+
syntaxErrors,
|
|
599
|
+
) {
|
|
600
|
+
const lines = [];
|
|
601
|
+
|
|
602
|
+
const syntaxLines = renderSyntaxErrors(syntaxErrors, styler);
|
|
603
|
+
if (syntaxLines.length > 0) {
|
|
604
|
+
lines.push('');
|
|
605
|
+
lines.push(...syntaxLines);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const importLines = renderImportErrors(
|
|
609
|
+
importErrors.rows || [],
|
|
610
|
+
importErrors.sharedDirectory,
|
|
611
|
+
Boolean(importErrors.directoryExists),
|
|
612
|
+
styler,
|
|
613
|
+
);
|
|
614
|
+
if (importLines.length > 0) {
|
|
615
|
+
lines.push('');
|
|
616
|
+
lines.push(...importLines);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (snapshot.errors.length > 0) {
|
|
620
|
+
lines.push('');
|
|
621
|
+
lines.push(
|
|
622
|
+
`${INDENT}${styler('red', SYMBOLS.error)} ${styler('red', pluralize(snapshot.errors.length, 'error'))}`,
|
|
623
|
+
);
|
|
624
|
+
lines.push(...renderDetailRows(snapshot.errors, projectDir, styler));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (snapshot.warnings.length > 0) {
|
|
628
|
+
lines.push('');
|
|
629
|
+
lines.push(
|
|
630
|
+
`${INDENT}${styler('yellow', SYMBOLS.warning)} ${styler('yellow', pluralize(snapshot.warnings.length, 'warning'))}`,
|
|
631
|
+
);
|
|
632
|
+
lines.push(...renderDetailRows(snapshot.warnings, projectDir, styler));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const assetLines = renderUnresolvedAssets(assetRows, styler);
|
|
636
|
+
if (assetLines.length > 0) {
|
|
637
|
+
lines.push('');
|
|
638
|
+
lines.push(...assetLines);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const deprecationLines = renderDeprecations(
|
|
642
|
+
snapshot,
|
|
643
|
+
projectDir,
|
|
644
|
+
styler,
|
|
645
|
+
sourceGlob,
|
|
646
|
+
);
|
|
647
|
+
if (deprecationLines.length > 0) {
|
|
648
|
+
lines.push('');
|
|
649
|
+
lines.push(...deprecationLines);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return lines;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Render the summary printed after the first successful watch build.
|
|
657
|
+
*
|
|
658
|
+
* @param {{
|
|
659
|
+
* snapshot: object,
|
|
660
|
+
* durationMs: number,
|
|
661
|
+
* outDir?: string,
|
|
662
|
+
* projectDir?: string,
|
|
663
|
+
* sourceGlob?: string,
|
|
664
|
+
* assetRows?: Array<object>,
|
|
665
|
+
* importErrors?: {rows?: Array<object>, sharedDirectory?: string, directoryExists?: boolean},
|
|
666
|
+
* styler: (format: string|string[], text: string) => string
|
|
667
|
+
* }} options - Summary inputs.
|
|
668
|
+
* @returns {string[]} Summary lines.
|
|
669
|
+
*/
|
|
670
|
+
export function renderSummary({
|
|
671
|
+
snapshot,
|
|
672
|
+
durationMs,
|
|
673
|
+
outDir = 'dist',
|
|
674
|
+
projectDir = '',
|
|
675
|
+
sourceGlob = 'src/**/*.scss',
|
|
676
|
+
assetRows = [],
|
|
677
|
+
importErrors = {},
|
|
678
|
+
syntaxErrors = [],
|
|
679
|
+
styler,
|
|
680
|
+
}) {
|
|
681
|
+
const failed =
|
|
682
|
+
snapshot.errors.length > 0 ||
|
|
683
|
+
(importErrors.rows || []).length > 0 ||
|
|
684
|
+
syntaxErrors.length > 0;
|
|
685
|
+
const symbol = failed
|
|
686
|
+
? styler('red', SYMBOLS.error)
|
|
687
|
+
: styler('green', SYMBOLS.ok);
|
|
688
|
+
const headline = failed
|
|
689
|
+
? `build failed after ${formatDuration(durationMs)}`
|
|
690
|
+
: `built in ${formatDuration(durationMs)}`;
|
|
691
|
+
|
|
692
|
+
const lines = [
|
|
693
|
+
`${INDENT}${symbol} ${headline}${styler('gray', `${SEPARATOR}watching ${outDir}`)}`,
|
|
694
|
+
...renderProblems(
|
|
695
|
+
snapshot,
|
|
696
|
+
projectDir,
|
|
697
|
+
styler,
|
|
698
|
+
sourceGlob,
|
|
699
|
+
assetRows,
|
|
700
|
+
importErrors,
|
|
701
|
+
syntaxErrors,
|
|
702
|
+
),
|
|
703
|
+
'',
|
|
704
|
+
];
|
|
705
|
+
|
|
706
|
+
return lines;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Render the compact line printed after each watch rebuild.
|
|
711
|
+
*
|
|
712
|
+
* @param {{
|
|
713
|
+
* snapshot: object,
|
|
714
|
+
* durationMs: number,
|
|
715
|
+
* changedFiles?: string[],
|
|
716
|
+
* projectDir?: string,
|
|
717
|
+
* styler: (format: string|string[], text: string) => string,
|
|
718
|
+
* now?: Date
|
|
719
|
+
* }} options - Rebuild inputs.
|
|
720
|
+
* @returns {string[]} Rebuild lines.
|
|
721
|
+
*/
|
|
722
|
+
export function renderRebuild({
|
|
723
|
+
snapshot,
|
|
724
|
+
durationMs,
|
|
725
|
+
changedFiles = [],
|
|
726
|
+
projectDir = '',
|
|
727
|
+
styler,
|
|
728
|
+
now = new Date(),
|
|
729
|
+
}) {
|
|
730
|
+
const failed = snapshot.errors.length > 0;
|
|
731
|
+
const [firstChange] = changedFiles;
|
|
732
|
+
const changeLabel =
|
|
733
|
+
changedFiles.length > 1
|
|
734
|
+
? `${displayLocation(firstChange, undefined, projectDir)} +${changedFiles.length - 1}`
|
|
735
|
+
: firstChange
|
|
736
|
+
? displayLocation(firstChange, undefined, projectDir)
|
|
737
|
+
: 'sources';
|
|
738
|
+
|
|
739
|
+
const outcome = failed
|
|
740
|
+
? styler('red', `rebuild failed after ${formatDuration(durationMs)}`)
|
|
741
|
+
: styler('gray', `rebuilt in ${formatDuration(durationMs)}`);
|
|
742
|
+
|
|
743
|
+
const symbol = failed
|
|
744
|
+
? styler('red', SYMBOLS.error)
|
|
745
|
+
: styler('gray', SYMBOLS.change);
|
|
746
|
+
|
|
747
|
+
const lines = [
|
|
748
|
+
`${INDENT}${styler('gray', formatClockTime(now))} ${symbol} ${changeLabel}${styler('gray', SEPARATOR)}${outcome}`,
|
|
749
|
+
];
|
|
750
|
+
|
|
751
|
+
// Repeating the deprecation tally on every keystroke would recreate the noise
|
|
752
|
+
// this reporter exists to remove, so rebuilds only surface hard failures.
|
|
753
|
+
if (failed) {
|
|
754
|
+
lines.push(...renderDetailRows(snapshot.errors, projectDir, styler));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return lines;
|
|
758
|
+
}
|