@emulsify/core 4.3.2 → 4.5.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-static-assets.js +5 -8
- package/.storybook/main-vite.js +11 -3
- package/README.md +14 -6
- package/config/a11y-wcag22.js +11 -0
- package/config/vite/entries.js +7 -2
- package/config/vite/environment.js +4 -0
- package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
- package/config/vite/plugins/assets/copy-src-assets.js +82 -12
- package/config/vite/plugins/assets/copy-twig-files.js +85 -16
- package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
- package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
- package/config/vite/plugins/assets/development-source-maps.js +273 -0
- package/config/vite/plugins/assets/mirror-components.js +98 -82
- package/config/vite/plugins/assets/output-freshness.js +235 -0
- package/config/vite/plugins/assets/source-file-index.js +13 -13
- package/config/vite/plugins/assets/stable-watch-output.js +165 -0
- package/config/vite/plugins/assets/storybook-output.js +27 -0
- package/config/vite/plugins/index.js +95 -9
- package/config/vite/plugins/reporter/asset-resolver.js +34 -6
- package/config/vite/plugins/reporter/build-errors.js +7 -3
- package/config/vite/plugins/reporter/diagnostics.js +140 -10
- package/config/vite/plugins/reporter/index.js +380 -75
- package/config/vite/plugins/reporter/render.js +297 -44
- package/config/vite/plugins/reporter/sass-logger.js +30 -0
- package/config/vite/plugins/reporter/source-roots.js +101 -21
- package/config/vite/plugins/reporter/strict-mode.js +99 -0
- package/config/vite/plugins/reporter/vite-logger.js +220 -8
- package/config/vite/plugins/reporter/watch-mode.js +6 -2
- package/config/vite/plugins/twig/twig-module.js +35 -258
- package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
- package/config/vite/project-config.js +121 -21
- package/config/vite/project-structure.js +6 -0
- package/config/vite/utils/asset-roots.js +205 -0
- package/config/vite/utils/css-urls.js +350 -0
- package/config/vite/utils/fs-safe.js +38 -1
- package/config/vite/utils/source-directory-skips.js +13 -0
- package/config/vite/utils/source-maps.js +88 -0
- package/config/vite/utils/twig-component-resolver.js +316 -0
- package/config/vite/vite.config.js +106 -42
- package/package.json +54 -40
- package/scripts/a11y.js +88 -9
- package/scripts/audit/checks/css-asset-references.js +256 -24
- package/scripts/audit/checks/twig-references.js +16 -5
- package/scripts/audit/fix.js +836 -0
- package/scripts/audit/index.js +10 -2
- package/scripts/audit/lib/css.js +41 -35
- package/scripts/audit/lib/story-ast.js +392 -0
- package/scripts/audit/lib/story-render-paths.js +600 -0
- package/scripts/audit/lib/story-selection.js +190 -0
- package/scripts/audit/lib/twig.js +372 -80
- package/scripts/audit/report.js +83 -5
- package/scripts/audit-twig-stories.js +73 -3
- package/scripts/audit.js +87 -2
- package/src/storybook/twig/source-function.js +14 -10
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared CSS and Sass `url()` tokenization.
|
|
3
|
+
*
|
|
4
|
+
* A regular expression can recognize the contents of `url()`, but it cannot
|
|
5
|
+
* tell whether the function-shaped text sits inside a comment or string. This
|
|
6
|
+
* scanner supplies that missing lexical context while preserving exact source
|
|
7
|
+
* offsets for both build rewrites and audit fixes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Characters that keep `url` inside a larger CSS identifier. */
|
|
11
|
+
const IDENT_CHAR_RE = /[\w\-\u0080-\uffff]/;
|
|
12
|
+
|
|
13
|
+
/** CSS whitespace accepted around a URL value. */
|
|
14
|
+
const WHITESPACE_RE = /\s/;
|
|
15
|
+
|
|
16
|
+
/** Quote delimiters accepted by CSS URL and string tokens. */
|
|
17
|
+
const SINGLE_QUOTE = String.fromCharCode(39);
|
|
18
|
+
const DOUBLE_QUOTE = '"';
|
|
19
|
+
|
|
20
|
+
/** URL values that cannot name a local file. */
|
|
21
|
+
const NON_FILESYSTEM_URL_RE = /^(?:#|\/\/|[a-z][a-z0-9+.-]*:|var\(|env\()/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Determine whether a character opens a quoted CSS value.
|
|
25
|
+
*
|
|
26
|
+
* @param {string|undefined} character - Candidate character.
|
|
27
|
+
* @returns {boolean} TRUE for a single or double quote.
|
|
28
|
+
*/
|
|
29
|
+
const isQuote = (character) =>
|
|
30
|
+
character === SINGLE_QUOTE || character === DOUBLE_QUOTE;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Determine whether a character ends an unterminated CSS string.
|
|
34
|
+
*
|
|
35
|
+
* CSS preprocessing treats carriage return, form feed, and line feed as
|
|
36
|
+
* newlines. Handling all three here keeps recovery correct before preprocessing.
|
|
37
|
+
*
|
|
38
|
+
* @param {string|undefined} character - Candidate character.
|
|
39
|
+
* @returns {boolean} TRUE for a CSS newline.
|
|
40
|
+
*/
|
|
41
|
+
const isCssNewline = (character) =>
|
|
42
|
+
character === '\n' || character === '\r' || character === '\f';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Determine whether a case-insensitive CSS `url(` function starts at an offset.
|
|
46
|
+
*
|
|
47
|
+
* Comparing individual characters avoids allocating and lowercasing a slice at
|
|
48
|
+
* every source position.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} source - Full stylesheet source.
|
|
51
|
+
* @param {number} start - Candidate `u` offset.
|
|
52
|
+
* @returns {boolean} TRUE when `url(` begins at the offset.
|
|
53
|
+
*/
|
|
54
|
+
const isUrlFunctionAt = (source, start) =>
|
|
55
|
+
(source[start] === 'u' || source[start] === 'U') &&
|
|
56
|
+
(source[start + 1] === 'r' || source[start + 1] === 'R') &&
|
|
57
|
+
(source[start + 2] === 'l' || source[start + 2] === 'L') &&
|
|
58
|
+
source[start + 3] === '(';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan a CSS string through its closing quote or an unescaped newline.
|
|
62
|
+
*
|
|
63
|
+
* A backslash-newline is a continuation, including the raw CRLF form that CSS
|
|
64
|
+
* preprocessing normally collapses before tokenization.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} source - Full stylesheet source.
|
|
67
|
+
* @param {number} cursor - First character after the opening quote.
|
|
68
|
+
* @param {string} quote - Opening quote character.
|
|
69
|
+
* @returns {number} Closing quote, newline, or EOF offset.
|
|
70
|
+
*/
|
|
71
|
+
function scanCssString(source, cursor, quote) {
|
|
72
|
+
while (
|
|
73
|
+
cursor < source.length &&
|
|
74
|
+
source[cursor] !== quote &&
|
|
75
|
+
!isCssNewline(source[cursor])
|
|
76
|
+
) {
|
|
77
|
+
if (source[cursor] !== '\\' || cursor + 1 >= source.length) {
|
|
78
|
+
cursor += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
cursor +=
|
|
83
|
+
source[cursor + 1] === '\r' && source[cursor + 2] === '\n' ? 3 : 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return cursor;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Trim a token value while keeping its offsets in the original source.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} source - Full stylesheet source.
|
|
93
|
+
* @param {number} start - Untrimmed value start.
|
|
94
|
+
* @param {number} end - Untrimmed value end.
|
|
95
|
+
* @returns {{value: string, valueStart: number, valueEnd: number}} Trimmed value and offsets.
|
|
96
|
+
*/
|
|
97
|
+
function trimValue(source, start, end) {
|
|
98
|
+
const untrimmed = source.slice(start, end);
|
|
99
|
+
const value = untrimmed.trim();
|
|
100
|
+
const leading = untrimmed.length - untrimmed.trimStart().length;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
value,
|
|
104
|
+
valueStart: start + leading,
|
|
105
|
+
valueEnd: start + leading + value.length,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a `url()` token beginning at an eligible source position.
|
|
111
|
+
*
|
|
112
|
+
* The argument is consumed atomically so `//` inside `url(https://...)` is not
|
|
113
|
+
* mistaken for a Sass comment. Backslash escapes keep quotes and closing
|
|
114
|
+
* parentheses from ending an argument early.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} source - Full stylesheet source.
|
|
117
|
+
* @param {number} start - Candidate `u` offset.
|
|
118
|
+
* @param {{invalidUnquotedUntil: number, noClosingParenthesisAfter: number}} scanState - Per-stylesheet failure memo.
|
|
119
|
+
* @returns {{start: number, end: number, valueStart: number, valueEnd: number, value: string, quote: string, match: string}|undefined} Parsed token.
|
|
120
|
+
*/
|
|
121
|
+
function urlTokenAt(source, start, scanState) {
|
|
122
|
+
if (!isUrlFunctionAt(source, start)) return undefined;
|
|
123
|
+
if (start > 0 && IDENT_CHAR_RE.test(source[start - 1])) return undefined;
|
|
124
|
+
|
|
125
|
+
const innerStart = start + 4;
|
|
126
|
+
let cursor = innerStart;
|
|
127
|
+
|
|
128
|
+
while (cursor < source.length && WHITESPACE_RE.test(source[cursor])) {
|
|
129
|
+
cursor += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const quote = isQuote(source[cursor]) ? source[cursor] : '';
|
|
133
|
+
let valueStart;
|
|
134
|
+
let valueEnd;
|
|
135
|
+
let value;
|
|
136
|
+
|
|
137
|
+
if (quote) {
|
|
138
|
+
const quotedValueStart = cursor + 1;
|
|
139
|
+
cursor = scanCssString(source, quotedValueStart, quote);
|
|
140
|
+
|
|
141
|
+
if (source[cursor] !== quote) return undefined;
|
|
142
|
+
|
|
143
|
+
({ value, valueStart, valueEnd } = trimValue(
|
|
144
|
+
source,
|
|
145
|
+
quotedValueStart,
|
|
146
|
+
cursor,
|
|
147
|
+
));
|
|
148
|
+
cursor += 1;
|
|
149
|
+
|
|
150
|
+
while (cursor < source.length && WHITESPACE_RE.test(source[cursor])) {
|
|
151
|
+
cursor += 1;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
cursor = innerStart;
|
|
155
|
+
|
|
156
|
+
// A prior unquoted candidate reached a quote without encountering `)`.
|
|
157
|
+
// Every later unquoted candidate inside that interval must hit the same
|
|
158
|
+
// quote first, so rescanning it cannot produce a token.
|
|
159
|
+
if (innerStart <= scanState.invalidUnquotedUntil) return undefined;
|
|
160
|
+
|
|
161
|
+
// A prior candidate already scanned this suffix to EOF without finding an
|
|
162
|
+
// unescaped `)`. Failing immediately keeps repeated malformed `url(` input
|
|
163
|
+
// linear instead of rescanning the same tail for every character.
|
|
164
|
+
if (innerStart >= scanState.noClosingParenthesisAfter) return undefined;
|
|
165
|
+
|
|
166
|
+
while (cursor < source.length && source[cursor] !== ')') {
|
|
167
|
+
if (isQuote(source[cursor])) {
|
|
168
|
+
scanState.invalidUnquotedUntil = Math.max(
|
|
169
|
+
scanState.invalidUnquotedUntil,
|
|
170
|
+
cursor,
|
|
171
|
+
);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
cursor += source[cursor] === '\\' && cursor + 1 < source.length ? 2 : 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (cursor >= source.length) {
|
|
178
|
+
scanState.noClosingParenthesisAfter = Math.min(
|
|
179
|
+
scanState.noClosingParenthesisAfter,
|
|
180
|
+
innerStart,
|
|
181
|
+
);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (cursor === innerStart) return undefined;
|
|
186
|
+
|
|
187
|
+
({ value, valueStart, valueEnd } = trimValue(source, innerStart, cursor));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (source[cursor] !== ')') return undefined;
|
|
191
|
+
|
|
192
|
+
const end = cursor + 1;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
start,
|
|
196
|
+
end,
|
|
197
|
+
valueStart,
|
|
198
|
+
valueEnd,
|
|
199
|
+
value,
|
|
200
|
+
quote,
|
|
201
|
+
match: source.slice(start, end),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Mask comments without changing any source offsets.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} source - Full stylesheet source.
|
|
209
|
+
* @param {Array<{start: number, end: number}>} comments - Comment ranges.
|
|
210
|
+
* @returns {string} Source with non-newline comment characters blanked.
|
|
211
|
+
*/
|
|
212
|
+
function maskComments(source, comments) {
|
|
213
|
+
if (!comments.length) return source;
|
|
214
|
+
|
|
215
|
+
const masked = source.split('');
|
|
216
|
+
|
|
217
|
+
for (const { start, end } of comments) {
|
|
218
|
+
for (let index = start; index < end; index += 1) {
|
|
219
|
+
if (masked[index] !== '\n') masked[index] = ' ';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return masked.join('');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Determine whether a stylesheet URL can never name a file on disk.
|
|
228
|
+
*
|
|
229
|
+
* Sass variables are meaningful in unquoted `url()` values. In a quoted value,
|
|
230
|
+
* `$` is ordinary filename data and must reach the filesystem classifier. An
|
|
231
|
+
* unresolved interpolation is skipped in either form because neither consumer
|
|
232
|
+
* can safely guess what path it represents.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} value - URL value without its surrounding quotes.
|
|
235
|
+
* @param {string} [quote=''] - Token quote, or an empty string when unquoted.
|
|
236
|
+
* @returns {boolean} TRUE when the value is not a filesystem path.
|
|
237
|
+
*/
|
|
238
|
+
export function isNonFilesystemCssUrl(value, quote = '') {
|
|
239
|
+
const trimmed = String(value || '').trim();
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
!trimmed ||
|
|
243
|
+
NON_FILESYSTEM_URL_RE.test(trimmed) ||
|
|
244
|
+
trimmed.includes('#{') ||
|
|
245
|
+
(!quote && trimmed.includes('$'))
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Tokenize real `url()` functions in CSS or Sass source.
|
|
251
|
+
*
|
|
252
|
+
* Block comments, `//` comments anywhere on a line, and ordinary quoted
|
|
253
|
+
* strings are skipped. Quoted values belonging to an actual `url()` token are
|
|
254
|
+
* consumed by that token instead of being mistaken for standalone strings.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} source - Stylesheet source.
|
|
257
|
+
* @returns {{urls: Array<{start: number, end: number, valueStart: number, valueEnd: number, value: string, quote: string, match: string}>, readonly sourceWithoutComments: string}} URL tokens and a lazily computed, offset-preserving comment mask.
|
|
258
|
+
*/
|
|
259
|
+
export function tokenizeStylesheetUrls(source) {
|
|
260
|
+
const urls = [];
|
|
261
|
+
const comments = [];
|
|
262
|
+
const scanState = {
|
|
263
|
+
invalidUnquotedUntil: -1,
|
|
264
|
+
noClosingParenthesisAfter: Number.POSITIVE_INFINITY,
|
|
265
|
+
};
|
|
266
|
+
let maskedSource;
|
|
267
|
+
let cursor = 0;
|
|
268
|
+
|
|
269
|
+
while (cursor < source.length) {
|
|
270
|
+
const next = source[cursor + 1];
|
|
271
|
+
|
|
272
|
+
if (source[cursor] === '/' && next === '*') {
|
|
273
|
+
const start = cursor;
|
|
274
|
+
cursor += 2;
|
|
275
|
+
|
|
276
|
+
// CSS consumes an unterminated block comment through EOF. Unlike a bad
|
|
277
|
+
// string, it deliberately does not recover at the next newline.
|
|
278
|
+
while (
|
|
279
|
+
cursor < source.length &&
|
|
280
|
+
!(source[cursor] === '*' && source[cursor + 1] === '/')
|
|
281
|
+
) {
|
|
282
|
+
cursor += 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
cursor = Math.min(source.length, cursor + 2);
|
|
286
|
+
comments.push({ start, end: cursor });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (source[cursor] === '/' && next === '/') {
|
|
291
|
+
const start = cursor;
|
|
292
|
+
cursor += 2;
|
|
293
|
+
while (cursor < source.length && source[cursor] !== '\n') cursor += 1;
|
|
294
|
+
comments.push({ start, end: cursor });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isQuote(source[cursor])) {
|
|
299
|
+
const quote = source[cursor];
|
|
300
|
+
cursor = scanCssString(source, cursor + 1, quote);
|
|
301
|
+
|
|
302
|
+
if (source[cursor] === quote) cursor += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const url = urlTokenAt(source, cursor, scanState);
|
|
307
|
+
if (url) {
|
|
308
|
+
urls.push(url);
|
|
309
|
+
cursor = url.end;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
cursor += 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
urls,
|
|
318
|
+
get sourceWithoutComments() {
|
|
319
|
+
maskedSource ??= maskComments(source, comments);
|
|
320
|
+
return maskedSource;
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Replace real stylesheet URL tokens without disturbing ignored lookalikes.
|
|
327
|
+
*
|
|
328
|
+
* @param {string} source - Stylesheet source.
|
|
329
|
+
* @param {(token: {start: number, end: number, valueStart: number, valueEnd: number, value: string, quote: string, match: string}) => string|undefined} replacer - Token callback.
|
|
330
|
+
* @returns {string} Rewritten source, or the original string when unchanged.
|
|
331
|
+
*/
|
|
332
|
+
export function replaceStylesheetUrlTokens(source, replacer) {
|
|
333
|
+
const { urls } = tokenizeStylesheetUrls(source);
|
|
334
|
+
let cursor = 0;
|
|
335
|
+
let rewritten = '';
|
|
336
|
+
let changed = false;
|
|
337
|
+
|
|
338
|
+
for (const token of urls) {
|
|
339
|
+
const replacement = replacer(token);
|
|
340
|
+
if (typeof replacement !== 'string' || replacement === token.match) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
rewritten += source.slice(cursor, token.start) + replacement;
|
|
345
|
+
cursor = token.end;
|
|
346
|
+
changed = true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return changed ? rewritten + source.slice(cursor) : source;
|
|
350
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @file Safe filesystem helpers for Vite config and scripts.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'fs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Determine whether a path exists without throwing on inaccessible files.
|
|
@@ -64,3 +64,40 @@ export function safeReadJson(filePath) {
|
|
|
64
64
|
export function firstExistingPath(candidates = []) {
|
|
65
65
|
return candidates.filter(Boolean).find((candidate) => safeExists(candidate));
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Return the first candidate that is a regular file.
|
|
70
|
+
*
|
|
71
|
+
* `statSync()` intentionally follows symlinks so a symlink to a file remains a
|
|
72
|
+
* valid asset reference while a directory with a file-like name does not.
|
|
73
|
+
*
|
|
74
|
+
* @param {string[]} candidates - Candidate filesystem paths.
|
|
75
|
+
* @returns {string|undefined} First file path, when found.
|
|
76
|
+
*/
|
|
77
|
+
export function firstExistingFile(candidates = []) {
|
|
78
|
+
return candidates.filter(Boolean).find((candidate) => {
|
|
79
|
+
try {
|
|
80
|
+
return statSync(candidate).isFile();
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve symlinks without throwing on missing or inaccessible paths.
|
|
89
|
+
*
|
|
90
|
+
* Used to tell "two asset roots that overlap" apart from "two different files
|
|
91
|
+
* with the same name", so an overlapping root configuration is not mistaken for
|
|
92
|
+
* an ambiguous asset reference.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} filePath - Absolute or relative filesystem path.
|
|
95
|
+
* @returns {string} Canonical path, or the input path when unavailable.
|
|
96
|
+
*/
|
|
97
|
+
export function safeRealPath(filePath) {
|
|
98
|
+
try {
|
|
99
|
+
return realpathSync(filePath);
|
|
100
|
+
} catch {
|
|
101
|
+
return filePath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Generated source-map filename helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { toPosixPath } from './paths.js';
|
|
9
|
+
|
|
10
|
+
/** URI schemes whose resolution is independent of the map's filesystem path. */
|
|
11
|
+
const URI_SCHEME_RE = /^[a-z][a-z\d+.-]*:/i;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Determine whether a build output name is a JavaScript or CSS source map.
|
|
15
|
+
*
|
|
16
|
+
* A generic `.map` suffix is not enough: `.map` is also a legitimate asset
|
|
17
|
+
* extension. Core emits JavaScript chunks, while CSS maps can be supplied by a
|
|
18
|
+
* project extension or another Vite pipeline.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} fileName - Output file name.
|
|
21
|
+
* @returns {boolean} TRUE for generated JavaScript and CSS source-map names.
|
|
22
|
+
*/
|
|
23
|
+
export function isGeneratedSourceMap(fileName) {
|
|
24
|
+
return /\.(?:[cm]?js|css)\.map$/i.test(String(fileName));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Rebase relative source paths when a generated map is moved on disk.
|
|
29
|
+
*
|
|
30
|
+
* Rollup writes component maps relative to `dist/components/**`. Drupal SDC
|
|
31
|
+
* then moves those files to project-root `components/**`; without rebasing,
|
|
32
|
+
* every relative source keeps one extra parent traversal and points outside
|
|
33
|
+
* the theme. Remote and virtual sources are left untouched. Maps with an
|
|
34
|
+
* explicit sourceRoot are conservatively preserved because that root may use
|
|
35
|
+
* URL rather than filesystem semantics.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} contents - JSON source-map contents.
|
|
38
|
+
* @param {string} sourceMapFile - Original map location.
|
|
39
|
+
* @param {string} destinationMapFile - Final map location.
|
|
40
|
+
* @returns {string} Rebased JSON, or the original contents when it is not safely rewritable.
|
|
41
|
+
*/
|
|
42
|
+
export function rebaseSourceMapForMove(
|
|
43
|
+
contents,
|
|
44
|
+
sourceMapFile,
|
|
45
|
+
destinationMapFile,
|
|
46
|
+
) {
|
|
47
|
+
let sourceMap;
|
|
48
|
+
try {
|
|
49
|
+
sourceMap = JSON.parse(contents);
|
|
50
|
+
} catch {
|
|
51
|
+
return contents;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!Array.isArray(sourceMap?.sources) || sourceMap.sourceRoot) {
|
|
55
|
+
return contents;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sourceDirectory = dirname(sourceMapFile);
|
|
59
|
+
const destinationDirectory = dirname(destinationMapFile);
|
|
60
|
+
let changed = false;
|
|
61
|
+
|
|
62
|
+
const sources = sourceMap.sources.map((source) => {
|
|
63
|
+
if (typeof source !== 'string' || !source || source.startsWith('\0')) {
|
|
64
|
+
return source;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let absoluteSource;
|
|
68
|
+
if (source.startsWith('file:')) {
|
|
69
|
+
try {
|
|
70
|
+
absoluteSource = fileURLToPath(source);
|
|
71
|
+
} catch {
|
|
72
|
+
return source;
|
|
73
|
+
}
|
|
74
|
+
} else if (isAbsolute(source)) {
|
|
75
|
+
absoluteSource = source;
|
|
76
|
+
} else {
|
|
77
|
+
if (URI_SCHEME_RE.test(source) || source.startsWith('//')) return source;
|
|
78
|
+
absoluteSource = resolve(sourceDirectory, source);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rebased = toPosixPath(relative(destinationDirectory, absoluteSource));
|
|
82
|
+
if (rebased !== source) changed = true;
|
|
83
|
+
return rebased;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!changed) return contents;
|
|
87
|
+
return `${JSON.stringify({ ...sourceMap, sources })}\n`;
|
|
88
|
+
}
|