@emulsify/core 4.2.1 → 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} +197 -85
- 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/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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Build error extraction for the Emulsify develop reporter.
|
|
3
|
+
*
|
|
4
|
+
* Rolldown reports a failed build as a single wrapper error whose message is
|
|
5
|
+
* `Build failed with N errors:` followed by every error rendered as text. Read
|
|
6
|
+
* naively, the reporter shows one error with a message that names a count and
|
|
7
|
+
* nothing else.
|
|
8
|
+
*
|
|
9
|
+
* The individual errors are reachable through the wrapper's `errors` property,
|
|
10
|
+
* and each one carries a `cause` chain that ends at the original Sass
|
|
11
|
+
* exception. That exception is fully structured — `span.url`, `span.start.line`
|
|
12
|
+
* and `span.text` give the file, the line, and the offending statement
|
|
13
|
+
* verbatim — so nothing has to be scraped out of formatted output.
|
|
14
|
+
*
|
|
15
|
+
* @see https://github.com/rolldown/rolldown - aggregateBindingErrorsIntoJsError
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Matches Sass's "missing import" failure.
|
|
22
|
+
*
|
|
23
|
+
* @type {RegExp}
|
|
24
|
+
*/
|
|
25
|
+
const MISSING_IMPORT = /Can't find stylesheet to import/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Captures the specifier from a `@use`, `@forward`, or `@import` statement.
|
|
29
|
+
*
|
|
30
|
+
* @type {RegExp}
|
|
31
|
+
*/
|
|
32
|
+
const IMPORT_SPECIFIER = /@(?:use|forward|import)\s+['"]([^'"]+)['"]/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Captures a line number and its source from a Sass code frame.
|
|
36
|
+
*
|
|
37
|
+
* Used only when the structured span is unavailable.
|
|
38
|
+
*
|
|
39
|
+
* @type {RegExp}
|
|
40
|
+
*/
|
|
41
|
+
const FRAME_LINE = /^\s*(\d+)\s*│\s?(.*)$/m;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Maximum depth walked through `cause` chains.
|
|
45
|
+
*
|
|
46
|
+
* @type {number}
|
|
47
|
+
*/
|
|
48
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Captures the minifier name Vite prefixes onto a CSS minification failure.
|
|
52
|
+
*
|
|
53
|
+
* Both the lightningcss and esbuild paths tag the message this way before
|
|
54
|
+
* rethrowing, and both attach `loc` and `frame` alongside it.
|
|
55
|
+
*
|
|
56
|
+
* @type {RegExp}
|
|
57
|
+
*/
|
|
58
|
+
const MINIFIER_PREFIX = /^\[((?:lightningcss|esbuild css) minify)\]\s*/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Captures one numbered row of a Vite code frame.
|
|
62
|
+
*
|
|
63
|
+
* @type {RegExp}
|
|
64
|
+
*/
|
|
65
|
+
const FRAME_ROW = /^\s*(\d+)\s*\|\s?(.*)$/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Captures the caret row that follows the offending row of a code frame.
|
|
69
|
+
*
|
|
70
|
+
* @type {RegExp}
|
|
71
|
+
*/
|
|
72
|
+
const FRAME_CARET = /^\s*\|\s?(\s*)\^/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a CSS minifier syntax failure.
|
|
76
|
+
*
|
|
77
|
+
* These are unlike every other error the reporter handles: the minifier runs
|
|
78
|
+
* on the concatenated bundle, so `loc.line` is a line of generated CSS and
|
|
79
|
+
* points at no file anyone wrote. What is salvageable is the offending
|
|
80
|
+
* declaration itself, which is enough to search the project for.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} error - Individual build error.
|
|
83
|
+
* @returns {{
|
|
84
|
+
* minifier: string,
|
|
85
|
+
* message: string,
|
|
86
|
+
* bundleLine: number|undefined,
|
|
87
|
+
* declaration: string|undefined,
|
|
88
|
+
* caretColumn: number|undefined
|
|
89
|
+
* }|undefined} Parsed syntax error.
|
|
90
|
+
*/
|
|
91
|
+
export function parseCssSyntaxError(error) {
|
|
92
|
+
const rawMessage = String(error?.message || '');
|
|
93
|
+
const prefix = MINIFIER_PREFIX.exec(rawMessage);
|
|
94
|
+
if (!prefix) return undefined;
|
|
95
|
+
|
|
96
|
+
const [firstLine] = rawMessage.replace(MINIFIER_PREFIX, '').split('\n');
|
|
97
|
+
const parsed = {
|
|
98
|
+
minifier: prefix[1],
|
|
99
|
+
message: firstLine.trim(),
|
|
100
|
+
bundleLine:
|
|
101
|
+
typeof error?.loc?.line === 'number' ? error.loc.line : undefined,
|
|
102
|
+
declaration: undefined,
|
|
103
|
+
caretColumn: undefined,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const rows = String(error?.frame || '').split('\n');
|
|
107
|
+
|
|
108
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
109
|
+
const row = FRAME_ROW.exec(rows[index]);
|
|
110
|
+
if (!row) continue;
|
|
111
|
+
|
|
112
|
+
// Without a bundle line, the row carrying a caret beneath it is the one.
|
|
113
|
+
const isOffending =
|
|
114
|
+
parsed.bundleLine == null
|
|
115
|
+
? FRAME_CARET.test(rows[index + 1] || '')
|
|
116
|
+
: Number(row[1]) === parsed.bundleLine;
|
|
117
|
+
if (!isOffending) continue;
|
|
118
|
+
|
|
119
|
+
const leading = row[2].length - row[2].trimStart().length;
|
|
120
|
+
parsed.declaration = row[2].trim();
|
|
121
|
+
|
|
122
|
+
const caret = FRAME_CARET.exec(rows[index + 1] || '');
|
|
123
|
+
if (caret) {
|
|
124
|
+
// Re-base the caret against the trimmed declaration.
|
|
125
|
+
parsed.caretColumn = Math.max(0, caret[1].length - leading);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Flatten a rolldown build error into the individual errors it wraps.
|
|
136
|
+
*
|
|
137
|
+
* @param {Error & {errors?: Array<object>}} error - Build error.
|
|
138
|
+
* @returns {Array<object>} Individual errors.
|
|
139
|
+
*/
|
|
140
|
+
export function flattenBuildErrors(error) {
|
|
141
|
+
if (!error) return [];
|
|
142
|
+
|
|
143
|
+
const nested = error.errors;
|
|
144
|
+
if (!Array.isArray(nested) || nested.length === 0) return [error];
|
|
145
|
+
|
|
146
|
+
return nested.flatMap((entry) => flattenBuildErrors(entry));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Walk an error's `cause` chain looking for an entry with a Sass span.
|
|
151
|
+
*
|
|
152
|
+
* @param {object} error - Error to inspect.
|
|
153
|
+
* @returns {object|undefined} Error carrying a Sass span.
|
|
154
|
+
*/
|
|
155
|
+
function findSpanned(error) {
|
|
156
|
+
let current = error;
|
|
157
|
+
|
|
158
|
+
for (let depth = 0; current && depth < MAX_CAUSE_DEPTH; depth += 1) {
|
|
159
|
+
if (current.span?.url || current.span?.start) return current;
|
|
160
|
+
current = current.cause;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Convert a Sass span URL to a filesystem path.
|
|
168
|
+
*
|
|
169
|
+
* @param {URL|string|undefined} url - Span URL.
|
|
170
|
+
* @returns {string|undefined} Filesystem path.
|
|
171
|
+
*/
|
|
172
|
+
function spanPath(url) {
|
|
173
|
+
if (!url) return undefined;
|
|
174
|
+
const href = typeof url === 'string' ? url : url.href;
|
|
175
|
+
if (!href?.startsWith('file:')) return href || undefined;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
return fileURLToPath(href);
|
|
179
|
+
} catch {
|
|
180
|
+
return href;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reduce a message to its first line, without the plugin prefix.
|
|
186
|
+
*
|
|
187
|
+
* @param {string|undefined} message - Raw message.
|
|
188
|
+
* @returns {string} Condensed message.
|
|
189
|
+
*/
|
|
190
|
+
function condense(message) {
|
|
191
|
+
const [first] = String(message || '').split('\n');
|
|
192
|
+
return first.replace(/^\[[^\]]+\]\s*/, '').trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Describe one build error in the terms the reporter renders.
|
|
197
|
+
*
|
|
198
|
+
* Prefers the structured Sass span and falls back to the rendered code frame,
|
|
199
|
+
* which is the only source of a line number when a plugin re-throws without
|
|
200
|
+
* preserving the original exception.
|
|
201
|
+
*
|
|
202
|
+
* @param {object} error - Individual build error.
|
|
203
|
+
* @returns {{
|
|
204
|
+
* message: string,
|
|
205
|
+
* file: string|undefined,
|
|
206
|
+
* line: number|undefined,
|
|
207
|
+
* statement: string|undefined,
|
|
208
|
+
* specifier: string|undefined,
|
|
209
|
+
* isMissingImport: boolean
|
|
210
|
+
* }} Description.
|
|
211
|
+
*/
|
|
212
|
+
export function describeBuildError(error) {
|
|
213
|
+
const spanned = findSpanned(error);
|
|
214
|
+
const message = condense(spanned?.message || error?.message);
|
|
215
|
+
|
|
216
|
+
let file = spanPath(spanned?.span?.url);
|
|
217
|
+
let line =
|
|
218
|
+
typeof spanned?.span?.start?.line === 'number'
|
|
219
|
+
? spanned.span.start.line + 1
|
|
220
|
+
: undefined;
|
|
221
|
+
let statement = spanned?.span?.text?.trim();
|
|
222
|
+
|
|
223
|
+
if (!file) file = error?.loc?.file || error?.id || undefined;
|
|
224
|
+
if (line == null && typeof error?.loc?.line === 'number') {
|
|
225
|
+
line = error.loc.line;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!statement && error?.frame) {
|
|
229
|
+
const framed = FRAME_LINE.exec(error.frame);
|
|
230
|
+
if (framed) {
|
|
231
|
+
statement = framed[2].trim();
|
|
232
|
+
if (line == null) line = Number(framed[1]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
message,
|
|
238
|
+
file,
|
|
239
|
+
line,
|
|
240
|
+
statement,
|
|
241
|
+
specifier: statement ? IMPORT_SPECIFIER.exec(statement)?.[1] : undefined,
|
|
242
|
+
isMissingImport: MISSING_IMPORT.test(message),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Split a build failure into missing imports and everything else.
|
|
248
|
+
*
|
|
249
|
+
* Missing imports get their own table because they share a shape — a file, a
|
|
250
|
+
* line, and a path that does not resolve — and because one deleted partial
|
|
251
|
+
* commonly produces a dozen of them.
|
|
252
|
+
*
|
|
253
|
+
* @param {Error & {errors?: Array<object>}} error - Build error.
|
|
254
|
+
* @returns {{
|
|
255
|
+
* importErrors: Array<object>,
|
|
256
|
+
* syntaxErrors: Array<object>,
|
|
257
|
+
* otherErrors: Array<object>
|
|
258
|
+
* }} Classified errors.
|
|
259
|
+
*/
|
|
260
|
+
export function classifyBuildError(error) {
|
|
261
|
+
const importErrors = [];
|
|
262
|
+
const syntaxErrors = [];
|
|
263
|
+
const otherErrors = [];
|
|
264
|
+
|
|
265
|
+
for (const entry of flattenBuildErrors(error)) {
|
|
266
|
+
const syntaxError = parseCssSyntaxError(entry);
|
|
267
|
+
if (syntaxError) {
|
|
268
|
+
syntaxErrors.push(syntaxError);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const described = describeBuildError(entry);
|
|
273
|
+
|
|
274
|
+
// A missing import with no specifier cannot be tabulated, so it falls
|
|
275
|
+
// through to the general error list rather than showing a blank row.
|
|
276
|
+
if (described.isMissingImport && described.specifier) {
|
|
277
|
+
importErrors.push(described);
|
|
278
|
+
} else {
|
|
279
|
+
otherErrors.push(described);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { importErrors, syntaxErrors, otherErrors };
|
|
284
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Diagnostic collection for the Emulsify develop reporter.
|
|
3
|
+
*
|
|
4
|
+
* Vite, Sass, and Rollup each report problems on their own schedule and in
|
|
5
|
+
* their own format. A single build cycle can emit the same Sass deprecation
|
|
6
|
+
* dozens of times, once per entry that transitively imports the offending
|
|
7
|
+
* partial, which is what makes the default `npm run develop` output unreadable.
|
|
8
|
+
*
|
|
9
|
+
* This module collects those reports into one per-cycle model that counts
|
|
10
|
+
* repeats instead of reprinting them. Deprecations are bucketed by deprecation
|
|
11
|
+
* ID, then by source location within that ID, so the reporter can say
|
|
12
|
+
* "slash-div, 20 occurrences across 1 file" rather than emitting 20 stack
|
|
13
|
+
* traces. Errors and plain warnings are deduplicated on the same key so a
|
|
14
|
+
* failure surfaced by two channels only reads as one problem.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const UNKNOWN_DEPRECATION_ID = 'unknown';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the deduplication key for a single reported source location.
|
|
21
|
+
*
|
|
22
|
+
* @param {string|undefined} file - Source file path, when known.
|
|
23
|
+
* @param {number|undefined} line - 1-based line number, when known.
|
|
24
|
+
* @returns {string} Stable location key.
|
|
25
|
+
*/
|
|
26
|
+
const locationKey = (file, line) =>
|
|
27
|
+
`${file || '<unknown>'}:${line == null ? '?' : line}`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the deduplication key for an error or plain warning.
|
|
31
|
+
*
|
|
32
|
+
* @param {{message?: string, file?: string, line?: number}} entry - Reported entry.
|
|
33
|
+
* @returns {string} Stable entry key.
|
|
34
|
+
*/
|
|
35
|
+
const entryKey = (entry) =>
|
|
36
|
+
`${locationKey(entry.file, entry.line)}|${entry.message || ''}`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Record one occurrence against a location map, incrementing when repeated.
|
|
40
|
+
*
|
|
41
|
+
* @param {Map<string, {file: string|undefined, line: number|undefined, count: number}>} locations - Location map.
|
|
42
|
+
* @param {string|undefined} file - Source file path.
|
|
43
|
+
* @param {number|undefined} line - 1-based line number.
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
const tallyLocation = (locations, file, line) => {
|
|
47
|
+
const key = locationKey(file, line);
|
|
48
|
+
const existing = locations.get(key);
|
|
49
|
+
|
|
50
|
+
if (existing) {
|
|
51
|
+
existing.count += 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
locations.set(key, { file, line, count: 1 });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sort locations by descending occurrence count, then by path for stability.
|
|
60
|
+
*
|
|
61
|
+
* @param {{file: string|undefined, line: number|undefined, count: number}} a - First location.
|
|
62
|
+
* @param {{file: string|undefined, line: number|undefined, count: number}} b - Second location.
|
|
63
|
+
* @returns {number} Comparator result.
|
|
64
|
+
*/
|
|
65
|
+
const byCountThenPath = (a, b) =>
|
|
66
|
+
b.count - a.count ||
|
|
67
|
+
locationKey(a.file, a.line).localeCompare(locationKey(b.file, b.line));
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Invert the deprecation buckets into a per-file worklist.
|
|
71
|
+
*
|
|
72
|
+
* The collector groups by deprecation ID because that is how Sass reports, but
|
|
73
|
+
* fixing the debt is a per-file activity: you open one partial and change the
|
|
74
|
+
* handful of lines inside it. This projection produces that view — each file
|
|
75
|
+
* with the offending lines beneath it — without disturbing the ID-keyed model
|
|
76
|
+
* the totals are computed from.
|
|
77
|
+
*
|
|
78
|
+
* Within a file the same deprecation is collapsed into one entry carrying every
|
|
79
|
+
* affected line. Ten `slash-div` hits across ten lines of one partial is one
|
|
80
|
+
* edit to make, not ten findings to read.
|
|
81
|
+
*
|
|
82
|
+
* @param {Array<{id: string, locations: Array<{file: string|undefined, line: number|undefined, count: number}>}>} deprecationList - ID-keyed buckets.
|
|
83
|
+
* @returns {Array<{file: string, occurrences: number, entries: Array<{id: string, count: number, lines: number[]}>}>} File-keyed worklist.
|
|
84
|
+
*/
|
|
85
|
+
const groupDeprecationsByFile = (deprecationList) => {
|
|
86
|
+
/** @type {Map<string, {file: string, occurrences: number, entries: Map<string, object>}>} */
|
|
87
|
+
const files = new Map();
|
|
88
|
+
|
|
89
|
+
for (const bucket of deprecationList) {
|
|
90
|
+
for (const location of bucket.locations) {
|
|
91
|
+
// A location with no file cannot be opened, so it has no place in a
|
|
92
|
+
// worklist. Those still count toward the headline totals.
|
|
93
|
+
if (!location.file) continue;
|
|
94
|
+
|
|
95
|
+
let group = files.get(location.file);
|
|
96
|
+
if (!group) {
|
|
97
|
+
group = { file: location.file, occurrences: 0, entries: new Map() };
|
|
98
|
+
files.set(location.file, group);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let entry = group.entries.get(bucket.id);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
entry = { id: bucket.id, count: 0, lines: [] };
|
|
104
|
+
group.entries.set(bucket.id, entry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
group.occurrences += location.count;
|
|
108
|
+
entry.count += location.count;
|
|
109
|
+
if (location.line != null && !entry.lines.includes(location.line)) {
|
|
110
|
+
entry.lines.push(location.line);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [...files.values()]
|
|
116
|
+
.map((group) => ({
|
|
117
|
+
file: group.file,
|
|
118
|
+
occurrences: group.occurrences,
|
|
119
|
+
entries: [...group.entries.values()]
|
|
120
|
+
.map((entry) => ({
|
|
121
|
+
...entry,
|
|
122
|
+
lines: [...entry.lines].sort((a, b) => a - b),
|
|
123
|
+
}))
|
|
124
|
+
.sort((a, b) => b.count - a.count || a.id.localeCompare(b.id)),
|
|
125
|
+
}))
|
|
126
|
+
.sort(
|
|
127
|
+
(a, b) => b.occurrences - a.occurrences || a.file.localeCompare(b.file),
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a per-cycle diagnostic collector.
|
|
133
|
+
*
|
|
134
|
+
* The collector is intentionally synchronous and process-local. The Sass logger
|
|
135
|
+
* and the reporter plugin both run inside the same Vite process, so they can
|
|
136
|
+
* share one instance directly without any cross-process coordination.
|
|
137
|
+
*
|
|
138
|
+
* @returns {{
|
|
139
|
+
* recordDeprecation: (entry: {id?: string, file?: string, line?: number}) => void,
|
|
140
|
+
* recordWarning: (entry: {message?: string, file?: string, line?: number}) => void,
|
|
141
|
+
* recordError: (entry: {message?: string, file?: string, line?: number}) => void,
|
|
142
|
+
* recordUnresolvedAsset: (entry: {url?: string, importer?: string}) => void,
|
|
143
|
+
* recordImportError: (entry: {file?: string, line?: number, specifier?: string}) => void,
|
|
144
|
+
* recordSyntaxError: (entry: {minifier?: string, message?: string, declaration?: string}) => void,
|
|
145
|
+
* hasCapturedBuildErrors: () => boolean,
|
|
146
|
+
* snapshot: () => {
|
|
147
|
+
* deprecations: Array<{id: string, occurrences: number, locations: Array<{file: string|undefined, line: number|undefined, count: number}>}>,
|
|
148
|
+
* deprecationsByFile: Array<{file: string, occurrences: number, entries: Array<{id: string, count: number, lines: number[]}>}>,
|
|
149
|
+
* unresolvedAssets: Array<{url: string, importer: string|undefined, count: number}>,
|
|
150
|
+
* importErrors: Array<{file: string|undefined, line: number|undefined, specifier: string, count: number}>,
|
|
151
|
+
* warnings: Array<{message: string|undefined, file: string|undefined, line: number|undefined, count: number}>,
|
|
152
|
+
* errors: Array<{message: string|undefined, file: string|undefined, line: number|undefined, count: number}>,
|
|
153
|
+
* deprecationTotal: number,
|
|
154
|
+
* deprecationFileCount: number,
|
|
155
|
+
* hasProblems: boolean
|
|
156
|
+
* },
|
|
157
|
+
* reset: () => void
|
|
158
|
+
* }} Diagnostic collector.
|
|
159
|
+
*/
|
|
160
|
+
export function createDiagnosticsCollector() {
|
|
161
|
+
/** @type {Map<string, {id: string, occurrences: number, locations: Map<string, object>}>} */
|
|
162
|
+
let deprecations = new Map();
|
|
163
|
+
/** @type {Map<string, object>} */
|
|
164
|
+
let warnings = new Map();
|
|
165
|
+
/** @type {Map<string, object>} */
|
|
166
|
+
let errors = new Map();
|
|
167
|
+
/** @type {Map<string, {url: string, importer: string|undefined, count: number}>} */
|
|
168
|
+
let unresolvedAssets = new Map();
|
|
169
|
+
/** @type {Map<string, object>} */
|
|
170
|
+
let importErrors = new Map();
|
|
171
|
+
/** @type {Map<string, object>} */
|
|
172
|
+
let syntaxErrors = new Map();
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Record one deprecation occurrence.
|
|
176
|
+
*
|
|
177
|
+
* @param {{id?: string, file?: string, line?: number}} entry - Deprecation details.
|
|
178
|
+
* @returns {void}
|
|
179
|
+
*/
|
|
180
|
+
const recordDeprecation = ({ id, file, line } = {}) => {
|
|
181
|
+
const deprecationId = id || UNKNOWN_DEPRECATION_ID;
|
|
182
|
+
let bucket = deprecations.get(deprecationId);
|
|
183
|
+
|
|
184
|
+
if (!bucket) {
|
|
185
|
+
bucket = { id: deprecationId, occurrences: 0, locations: new Map() };
|
|
186
|
+
deprecations.set(deprecationId, bucket);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
bucket.occurrences += 1;
|
|
190
|
+
tallyLocation(bucket.locations, file, line);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Record one entry against a deduplicating map.
|
|
195
|
+
*
|
|
196
|
+
* @param {Map<string, object>} target - Destination map.
|
|
197
|
+
* @param {{message?: string, file?: string, line?: number}} entry - Entry details.
|
|
198
|
+
* @returns {void}
|
|
199
|
+
*/
|
|
200
|
+
const recordEntry = (target, entry = {}) => {
|
|
201
|
+
const key = entryKey(entry);
|
|
202
|
+
const existing = target.get(key);
|
|
203
|
+
|
|
204
|
+
if (existing) {
|
|
205
|
+
existing.count += 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
target.set(key, { ...entry, count: 1 });
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
recordDeprecation,
|
|
214
|
+
|
|
215
|
+
recordWarning: (entry) => recordEntry(warnings, entry),
|
|
216
|
+
|
|
217
|
+
recordError: (entry) => recordEntry(errors, entry),
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Record one Sass import that could not be resolved.
|
|
221
|
+
*
|
|
222
|
+
* Keyed by the importing site and the specifier, because the same missing
|
|
223
|
+
* partial reported through two entrypoints is one thing to fix.
|
|
224
|
+
*
|
|
225
|
+
* @param {{file?: string, line?: number, specifier?: string, statement?: string}} entry - Import error.
|
|
226
|
+
* @returns {void}
|
|
227
|
+
*/
|
|
228
|
+
recordImportError(entry = {}) {
|
|
229
|
+
if (!entry.specifier) return;
|
|
230
|
+
|
|
231
|
+
const key = `${entry.file || '?'}:${entry.line ?? '?'}|${entry.specifier}`;
|
|
232
|
+
const existing = importErrors.get(key);
|
|
233
|
+
|
|
234
|
+
if (existing) {
|
|
235
|
+
existing.count += 1;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
importErrors.set(key, { ...entry, count: 1 });
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Record one CSS minifier syntax failure.
|
|
244
|
+
*
|
|
245
|
+
* Keyed by the offending declaration, since the same broken rule reached
|
|
246
|
+
* through several entrypoints is one thing to fix.
|
|
247
|
+
*
|
|
248
|
+
* @param {{minifier?: string, message?: string, declaration?: string}} entry - Syntax error.
|
|
249
|
+
* @returns {void}
|
|
250
|
+
*/
|
|
251
|
+
recordSyntaxError(entry = {}) {
|
|
252
|
+
if (!entry.message) return;
|
|
253
|
+
|
|
254
|
+
const key = `${entry.declaration || ''}|${entry.message}`;
|
|
255
|
+
const existing = syntaxErrors.get(key);
|
|
256
|
+
|
|
257
|
+
if (existing) {
|
|
258
|
+
existing.count += 1;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
syntaxErrors.set(key, { ...entry, count: 1 });
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Record one CSS `url()` that Vite could not resolve at build time.
|
|
267
|
+
*
|
|
268
|
+
* Keyed by URL, because the same asset referenced from two stylesheets with
|
|
269
|
+
* different relative paths is two separate things for an author to fix.
|
|
270
|
+
*
|
|
271
|
+
* @param {{url?: string, importer?: string}} entry - Unresolved asset.
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
recordUnresolvedAsset({ url, importer } = {}) {
|
|
275
|
+
if (!url) return;
|
|
276
|
+
|
|
277
|
+
const existing = unresolvedAssets.get(url);
|
|
278
|
+
if (existing) {
|
|
279
|
+
existing.count += 1;
|
|
280
|
+
existing.importer = existing.importer || importer;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
unresolvedAssets.set(url, { url, importer, count: 1 });
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
snapshot() {
|
|
288
|
+
const deprecationList = [...deprecations.values()]
|
|
289
|
+
.map((bucket) => ({
|
|
290
|
+
id: bucket.id,
|
|
291
|
+
occurrences: bucket.occurrences,
|
|
292
|
+
locations: [...bucket.locations.values()].sort(byCountThenPath),
|
|
293
|
+
}))
|
|
294
|
+
.sort(
|
|
295
|
+
(a, b) => b.occurrences - a.occurrences || a.id.localeCompare(b.id),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const distinctFiles = new Set();
|
|
299
|
+
for (const bucket of deprecationList) {
|
|
300
|
+
for (const location of bucket.locations) {
|
|
301
|
+
if (location.file) distinctFiles.add(location.file);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const errorList = [...errors.values()];
|
|
306
|
+
const warningList = [...warnings.values()];
|
|
307
|
+
const unresolvedAssetList = [...unresolvedAssets.values()].sort(
|
|
308
|
+
(a, b) => b.count - a.count || a.url.localeCompare(b.url),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const importErrorList = [...importErrors.values()].sort(
|
|
312
|
+
(a, b) =>
|
|
313
|
+
String(a.file).localeCompare(String(b.file)) ||
|
|
314
|
+
(a.line ?? 0) - (b.line ?? 0),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
deprecations: deprecationList,
|
|
319
|
+
deprecationsByFile: groupDeprecationsByFile(deprecationList),
|
|
320
|
+
unresolvedAssets: unresolvedAssetList,
|
|
321
|
+
importErrors: importErrorList,
|
|
322
|
+
syntaxErrors: [...syntaxErrors.values()],
|
|
323
|
+
warnings: warningList,
|
|
324
|
+
errors: errorList,
|
|
325
|
+
deprecationTotal: deprecationList.reduce(
|
|
326
|
+
(total, bucket) => total + bucket.occurrences,
|
|
327
|
+
0,
|
|
328
|
+
),
|
|
329
|
+
deprecationFileCount: distinctFiles.size,
|
|
330
|
+
hasProblems:
|
|
331
|
+
errorList.length > 0 ||
|
|
332
|
+
warningList.length > 0 ||
|
|
333
|
+
deprecationList.length > 0 ||
|
|
334
|
+
unresolvedAssetList.length > 0 ||
|
|
335
|
+
importErrorList.length > 0 ||
|
|
336
|
+
syntaxErrors.size > 0,
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Whether any build error has been captured this cycle.
|
|
342
|
+
*
|
|
343
|
+
* The logger consults this before swallowing Rolldown's raw error dump, so
|
|
344
|
+
* output is only suppressed once the reporter has something to show in its
|
|
345
|
+
* place.
|
|
346
|
+
*
|
|
347
|
+
* Deliberately covers ordinary errors as well as missing imports. Scoping
|
|
348
|
+
* it to imports alone meant a failure of any other kind — an undefined
|
|
349
|
+
* mixin, say — was reported by the summary *and* dumped raw, so the one
|
|
350
|
+
* error appeared twice.
|
|
351
|
+
*
|
|
352
|
+
* @returns {boolean} TRUE when any build error was captured.
|
|
353
|
+
*/
|
|
354
|
+
hasCapturedBuildErrors: () =>
|
|
355
|
+
importErrors.size > 0 || errors.size > 0 || syntaxErrors.size > 0,
|
|
356
|
+
|
|
357
|
+
reset() {
|
|
358
|
+
deprecations = new Map();
|
|
359
|
+
warnings = new Map();
|
|
360
|
+
errors = new Map();
|
|
361
|
+
unresolvedAssets = new Map();
|
|
362
|
+
importErrors = new Map();
|
|
363
|
+
syntaxErrors = new Map();
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|