@harness-fe/unplugin 3.0.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/core.d.ts +19 -0
- package/dist/core.js +211 -0
- package/dist/esbuild.d.ts +9 -0
- package/dist/esbuild.js +9 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/internal/buildIdentity.d.ts +19 -0
- package/dist/internal/buildIdentity.js +49 -0
- package/dist/internal/log-capture.d.ts +7 -0
- package/dist/internal/log-capture.js +22 -0
- package/dist/internal/mcp-client.d.ts +11 -0
- package/dist/internal/mcp-client.js +165 -0
- package/dist/internal/types.d.ts +61 -0
- package/dist/internal/types.js +4 -0
- package/dist/resolveBuildId.d.ts +32 -0
- package/dist/resolveBuildId.js +88 -0
- package/dist/resolveProjectId.d.ts +9 -0
- package/dist/resolveProjectId.js +44 -0
- package/dist/rollup.d.ts +9 -0
- package/dist/rollup.js +9 -0
- package/dist/rspack.d.ts +9 -0
- package/dist/rspack.js +9 -0
- package/dist/transform.d.ts +27 -0
- package/dist/transform.js +150 -0
- package/dist/vite.d.ts +10 -0
- package/dist/vite.js +10 -0
- package/dist/vue-transform.d.ts +90 -0
- package/dist/vue-transform.js +350 -0
- package/package.json +75 -0
- package/src/core.ts +230 -0
- package/src/esbuild.ts +12 -0
- package/src/index.ts +34 -0
- package/src/internal/buildIdentity.ts +66 -0
- package/src/internal/log-capture.ts +26 -0
- package/src/internal/mcp-client.ts +181 -0
- package/src/internal/types.ts +66 -0
- package/src/resolveBuildId.test.ts +63 -0
- package/src/resolveBuildId.ts +125 -0
- package/src/resolveProjectId.test.ts +99 -0
- package/src/resolveProjectId.ts +48 -0
- package/src/rollup.ts +12 -0
- package/src/rspack.ts +12 -0
- package/src/transform.test.ts +89 -0
- package/src/transform.ts +188 -0
- package/src/vite.ts +13 -0
- package/src/vue-transform.test.ts +398 -0
- package/src/vue-transform.ts +455 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue SFC transform: parse a .vue file and inject:
|
|
3
|
+
* - `data-morphix-loc="<relPath>:<line>:<col>"` on every template element
|
|
4
|
+
* - `data-morphix-comp="<ComponentName>"` on every template element
|
|
5
|
+
*
|
|
6
|
+
* Uses @vue/compiler-sfc to parse the SFC and @vue/compiler-dom to walk the
|
|
7
|
+
* template AST. MagicString splices attributes into the original source to
|
|
8
|
+
* preserve source maps.
|
|
9
|
+
*
|
|
10
|
+
* Side effect: every successfully scanned file contributes entries to the
|
|
11
|
+
* supplied `componentMap`: name → list of locations (file:line:col).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parse as parseSFC } from '@vue/compiler-sfc';
|
|
15
|
+
import { parse as parseTemplate } from '@vue/compiler-dom';
|
|
16
|
+
import MagicString from 'magic-string';
|
|
17
|
+
import type { ComponentMap } from './transform.js';
|
|
18
|
+
|
|
19
|
+
export interface VueTransformResult {
|
|
20
|
+
code: string;
|
|
21
|
+
map?: object;
|
|
22
|
+
taggedCount: number;
|
|
23
|
+
componentName: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Counters maintained across calls — populated even in dry-run mode. The
|
|
28
|
+
* unplugin core attaches a single instance per dev-server lifetime and
|
|
29
|
+
* dumps it on process exit so users can see how many Vue 2-era files were
|
|
30
|
+
* skipped (filter syntax, functional templates, malformed offsets, …).
|
|
31
|
+
*/
|
|
32
|
+
export interface VueTransformStats {
|
|
33
|
+
filesAttempted: number;
|
|
34
|
+
filesInjected: number;
|
|
35
|
+
elementsTagged: number;
|
|
36
|
+
skippedSfcError: number;
|
|
37
|
+
skippedTemplateError: number;
|
|
38
|
+
skippedWalkError: number;
|
|
39
|
+
skippedSelfCheck: number;
|
|
40
|
+
/** Sample of skipped file paths (capped at 50 to bound memory). */
|
|
41
|
+
skippedPaths: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createVueTransformStats(): VueTransformStats {
|
|
45
|
+
return {
|
|
46
|
+
filesAttempted: 0,
|
|
47
|
+
filesInjected: 0,
|
|
48
|
+
elementsTagged: 0,
|
|
49
|
+
skippedSfcError: 0,
|
|
50
|
+
skippedTemplateError: 0,
|
|
51
|
+
skippedWalkError: 0,
|
|
52
|
+
skippedSelfCheck: 0,
|
|
53
|
+
skippedPaths: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface VueTransformOptions {
|
|
58
|
+
/**
|
|
59
|
+
* When true (default), the transform re-parses its own output before
|
|
60
|
+
* returning it. Catches MagicString offset bugs against malformed Vue
|
|
61
|
+
* 2-era syntax before vue-loader ever sees them.
|
|
62
|
+
*/
|
|
63
|
+
safeMode?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* When true, walk the AST and populate the componentMap as usual, but
|
|
66
|
+
* always return null (no source injection). Used by the dry-run
|
|
67
|
+
* coverage report.
|
|
68
|
+
*/
|
|
69
|
+
dryRun?: boolean;
|
|
70
|
+
/** Counters to update; ignored if omitted. */
|
|
71
|
+
stats?: VueTransformStats;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SKIP_PATH_CAP = 50;
|
|
75
|
+
|
|
76
|
+
type SkipKind = 'skippedSfcError' | 'skippedTemplateError' | 'skippedWalkError' | 'skippedSelfCheck';
|
|
77
|
+
|
|
78
|
+
function recordSkip(stats: VueTransformStats | undefined, kind: SkipKind, relPath: string): void {
|
|
79
|
+
if (!stats) return;
|
|
80
|
+
stats[kind] += 1;
|
|
81
|
+
if (stats.skippedPaths.length < SKIP_PATH_CAP) {
|
|
82
|
+
stats.skippedPaths.push(relPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ATTR_COMP = 'data-morphix-comp';
|
|
87
|
+
const ATTR_LOC = 'data-morphix-loc';
|
|
88
|
+
|
|
89
|
+
/** Node types from @vue/compiler-dom */
|
|
90
|
+
const NODE_ELEMENT = 1;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert a filename (without extension) to PascalCase.
|
|
94
|
+
* e.g. "my-component" → "MyComponent", "hello_world" → "HelloWorld"
|
|
95
|
+
*/
|
|
96
|
+
function toPascalCase(str: string): string {
|
|
97
|
+
return str
|
|
98
|
+
.replace(/[-_]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
99
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the component name from a Vue SFC.
|
|
104
|
+
*
|
|
105
|
+
* Priority:
|
|
106
|
+
* 1. defineOptions({ name: '...' }) in <script setup>
|
|
107
|
+
* 2. export default { name: '...' } in <script>
|
|
108
|
+
* 3. PascalCase of filename (without .vue)
|
|
109
|
+
* 4. If filename is index.vue, PascalCase of parent directory
|
|
110
|
+
*/
|
|
111
|
+
function resolveComponentName(
|
|
112
|
+
descriptor: { script?: { content: string } | null; scriptSetup?: { content: string } | null },
|
|
113
|
+
relPath: string,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
// 1. Check <script setup> for defineOptions({ name: '...' })
|
|
116
|
+
if (descriptor.scriptSetup?.content) {
|
|
117
|
+
const match = descriptor.scriptSetup.content.match(
|
|
118
|
+
/defineOptions\s*\(\s*\{[^}]*name\s*:\s*['"]([^'"]+)['"]/,
|
|
119
|
+
);
|
|
120
|
+
if (match) return match[1];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2. Check <script> for export default { name: '...' }
|
|
124
|
+
if (descriptor.script?.content) {
|
|
125
|
+
const match = descriptor.script.content.match(
|
|
126
|
+
/export\s+default\s*\{[^}]*name\s*:\s*['"]([^'"]+)['"]/,
|
|
127
|
+
);
|
|
128
|
+
if (match) return match[1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Fallback to filename
|
|
132
|
+
const parts = relPath.replace(/\\/g, '/').split('/');
|
|
133
|
+
const filename = parts[parts.length - 1];
|
|
134
|
+
const basename = filename.replace(/\.vue$/, '');
|
|
135
|
+
|
|
136
|
+
// 4. If index.vue, use parent directory name
|
|
137
|
+
if (basename.toLowerCase() === 'index') {
|
|
138
|
+
const parentDir = parts.length >= 2 ? parts[parts.length - 2] : undefined;
|
|
139
|
+
if (parentDir) return toPascalCase(parentDir);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return toPascalCase(basename);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function escapeAttr(value: string): string {
|
|
147
|
+
return value.replace(/"/g, '"');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface TemplateNode {
|
|
151
|
+
type: number;
|
|
152
|
+
tag?: string;
|
|
153
|
+
props?: Array<{ name: string }>;
|
|
154
|
+
children?: TemplateNode[];
|
|
155
|
+
loc: {
|
|
156
|
+
start: { line: number; column: number; offset: number };
|
|
157
|
+
end: { line: number; column: number; offset: number };
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Inject `data-morphix-*` attributes into a raw Vue template HTML fragment.
|
|
163
|
+
*
|
|
164
|
+
* Used by the webpack pipeline to handle the `*.vue?vue&type=template` virtual
|
|
165
|
+
* sub-module emitted by vue-loader. vue-loader's `templateLoader` will then
|
|
166
|
+
* compile the (now-tagged) template into a render function, preserving the
|
|
167
|
+
* attributes on every element vnode.
|
|
168
|
+
*
|
|
169
|
+
* `lineOffset` is added to every element's reported line number — pass the
|
|
170
|
+
* 1-based line index where this template appears in the original `.vue` file
|
|
171
|
+
* (so locations remain file-relative, not template-relative).
|
|
172
|
+
*/
|
|
173
|
+
export function transformVueTemplate(
|
|
174
|
+
templateSource: string,
|
|
175
|
+
relPath: string,
|
|
176
|
+
componentName: string | undefined,
|
|
177
|
+
componentMap: ComponentMap,
|
|
178
|
+
lineOffset: number = 0,
|
|
179
|
+
options: VueTransformOptions = {},
|
|
180
|
+
): { code: string; map?: object; taggedCount: number } | null {
|
|
181
|
+
const safeMode = options.safeMode !== false;
|
|
182
|
+
const stats = options.stats;
|
|
183
|
+
if (stats) stats.filesAttempted++;
|
|
184
|
+
|
|
185
|
+
let ast;
|
|
186
|
+
try {
|
|
187
|
+
ast = parseTemplate(templateSource);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.warn(`[harness-fe] Failed to parse Vue template fragment: ${relPath}`, err);
|
|
190
|
+
recordSkip(stats, 'skippedTemplateError', relPath);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const magic = new MagicString(templateSource);
|
|
195
|
+
let taggedCount = 0;
|
|
196
|
+
|
|
197
|
+
function walkNode(node: TemplateNode): void {
|
|
198
|
+
if (node.type === NODE_ELEMENT && node.tag) {
|
|
199
|
+
const line = node.loc.start.line + lineOffset;
|
|
200
|
+
const col = node.loc.start.column;
|
|
201
|
+
const locValue = `${relPath}:${line}:${col}`;
|
|
202
|
+
|
|
203
|
+
const hasLoc = node.props?.some((p) => p.name === ATTR_LOC) ?? false;
|
|
204
|
+
const hasComp = node.props?.some((p) => p.name === ATTR_COMP) ?? false;
|
|
205
|
+
|
|
206
|
+
const attrs: string[] = [];
|
|
207
|
+
if (!hasLoc) attrs.push(`${ATTR_LOC}="${escapeAttr(locValue)}"`);
|
|
208
|
+
if (!hasComp && componentName)
|
|
209
|
+
attrs.push(`${ATTR_COMP}="${escapeAttr(componentName)}"`);
|
|
210
|
+
|
|
211
|
+
if (attrs.length > 0) {
|
|
212
|
+
// Position after the tag name in the original template fragment.
|
|
213
|
+
const tagNameEnd = node.loc.start.offset + 1 + node.tag.length;
|
|
214
|
+
magic.appendLeft(tagNameEnd, ' ' + attrs.join(' '));
|
|
215
|
+
taggedCount++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (componentName) {
|
|
219
|
+
const entries = componentMap.get(componentName) ?? [];
|
|
220
|
+
entries.push({ file: relPath, line, col });
|
|
221
|
+
componentMap.set(componentName, entries);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (node.children) for (const child of node.children) walkNode(child);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
for (const child of ast.children) walkNode(child as TemplateNode);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.warn(`[harness-fe] template walk failed in ${relPath}`, err);
|
|
231
|
+
recordSkip(stats, 'skippedWalkError', relPath);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (taggedCount === 0) return null;
|
|
236
|
+
|
|
237
|
+
const code = magic.toString();
|
|
238
|
+
|
|
239
|
+
// SafeMode self-check: re-parse our output to make sure we didn't
|
|
240
|
+
// produce something vue-loader will choke on. Cheap insurance — Vue 2
|
|
241
|
+
// legacy syntax is the typical reason this fires.
|
|
242
|
+
if (safeMode) {
|
|
243
|
+
try {
|
|
244
|
+
parseTemplate(code);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.warn(
|
|
247
|
+
`[harness-fe] safeMode dropped template injection in ${relPath} (self-check failed)`,
|
|
248
|
+
err,
|
|
249
|
+
);
|
|
250
|
+
recordSkip(stats, 'skippedSelfCheck', relPath);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (options.dryRun) {
|
|
256
|
+
if (stats) stats.elementsTagged += taggedCount;
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (stats) {
|
|
261
|
+
stats.filesInjected++;
|
|
262
|
+
stats.elementsTagged += taggedCount;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
code,
|
|
267
|
+
map: magic.generateMap({ hires: true, source: relPath, includeContent: true }),
|
|
268
|
+
taggedCount,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolve the component name from a raw .vue source (used by webpack pipeline
|
|
274
|
+
* where we only see the template sub-module and need to look up the parent's
|
|
275
|
+
* component name from disk).
|
|
276
|
+
*/
|
|
277
|
+
export function resolveVueComponentName(source: string, relPath: string): string | undefined {
|
|
278
|
+
try {
|
|
279
|
+
const { descriptor } = parseSFC(source, { filename: relPath });
|
|
280
|
+
return resolveComponentName(descriptor, relPath);
|
|
281
|
+
} catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Compute the 0-based line offset where the `<template>` *content* begins in
|
|
288
|
+
* the original .vue file. Adding this to template-relative line numbers gives
|
|
289
|
+
* file-relative numbers suitable for `data-morphix-loc`.
|
|
290
|
+
*
|
|
291
|
+
* Returns 0 if the SFC cannot be parsed or has no template block.
|
|
292
|
+
*/
|
|
293
|
+
export function getTemplateLineOffset(source: string, relPath: string): number {
|
|
294
|
+
try {
|
|
295
|
+
const { descriptor } = parseSFC(source, { filename: relPath });
|
|
296
|
+
if (!descriptor.template) return 0;
|
|
297
|
+
// descriptor.template.loc.start is 1-based and points at the FIRST char
|
|
298
|
+
// INSIDE <template> (i.e., the character after the closing `>`).
|
|
299
|
+
// We subtract 1 so that template-relative line 1 maps to that source line.
|
|
300
|
+
return descriptor.template.loc.start.line - 1;
|
|
301
|
+
} catch {
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function transformVueSFC(
|
|
307
|
+
source: string,
|
|
308
|
+
relPath: string,
|
|
309
|
+
componentMap: ComponentMap,
|
|
310
|
+
options: VueTransformOptions = {},
|
|
311
|
+
): VueTransformResult | null {
|
|
312
|
+
const safeMode = options.safeMode !== false;
|
|
313
|
+
const stats = options.stats;
|
|
314
|
+
if (stats) stats.filesAttempted++;
|
|
315
|
+
|
|
316
|
+
let descriptor;
|
|
317
|
+
try {
|
|
318
|
+
const result = parseSFC(source, { filename: relPath });
|
|
319
|
+
// Strict downgrade: if @vue/compiler-sfc surfaces any errors we don't
|
|
320
|
+
// trust the offsets it reports either. Skip the file entirely so
|
|
321
|
+
// vue-loader sees pristine source.
|
|
322
|
+
if (result.errors.length > 0) {
|
|
323
|
+
console.warn(`[harness-fe] Vue SFC parse errors in ${relPath}:`, result.errors);
|
|
324
|
+
recordSkip(stats, 'skippedSfcError', relPath);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
descriptor = result.descriptor;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.warn(`[harness-fe] Failed to parse Vue SFC: ${relPath}`, err);
|
|
330
|
+
recordSkip(stats, 'skippedSfcError', relPath);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!descriptor.template) return null;
|
|
335
|
+
|
|
336
|
+
const componentName = resolveComponentName(descriptor, relPath);
|
|
337
|
+
|
|
338
|
+
const templateContent = descriptor.template.content;
|
|
339
|
+
let templateAst;
|
|
340
|
+
try {
|
|
341
|
+
templateAst = parseTemplate(templateContent);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.warn(`[harness-fe] Failed to parse template in ${relPath}`, err);
|
|
344
|
+
recordSkip(stats, 'skippedTemplateError', relPath);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const magic = new MagicString(source);
|
|
349
|
+
const templateOffset = descriptor.template.loc.start.offset;
|
|
350
|
+
let taggedCount = 0;
|
|
351
|
+
|
|
352
|
+
function walkNode(node: TemplateNode): void {
|
|
353
|
+
if (node.type === NODE_ELEMENT && node.tag) {
|
|
354
|
+
const line = node.loc.start.line;
|
|
355
|
+
const col = node.loc.start.column;
|
|
356
|
+
const locValue = `${relPath}:${line}:${col}`;
|
|
357
|
+
|
|
358
|
+
const hasLoc = node.props?.some((p) => p.name === ATTR_LOC) ?? false;
|
|
359
|
+
const hasComp = node.props?.some((p) => p.name === ATTR_COMP) ?? false;
|
|
360
|
+
|
|
361
|
+
const attrs: string[] = [];
|
|
362
|
+
if (!hasLoc) attrs.push(`${ATTR_LOC}="${escapeAttr(locValue)}"`);
|
|
363
|
+
if (!hasComp && componentName)
|
|
364
|
+
attrs.push(`${ATTR_COMP}="${escapeAttr(componentName)}"`);
|
|
365
|
+
|
|
366
|
+
if (attrs.length > 0) {
|
|
367
|
+
const tagNameEnd = templateOffset + node.loc.start.offset + 1 + node.tag.length;
|
|
368
|
+
magic.appendLeft(tagNameEnd, ' ' + attrs.join(' '));
|
|
369
|
+
taggedCount++;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (componentName) {
|
|
373
|
+
const entries = componentMap.get(componentName) ?? [];
|
|
374
|
+
entries.push({ file: relPath, line, col });
|
|
375
|
+
componentMap.set(componentName, entries);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (node.children) {
|
|
380
|
+
for (const child of node.children) walkNode(child);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
for (const child of templateAst.children) walkNode(child as TemplateNode);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.warn(`[harness-fe] SFC walk failed in ${relPath}`, err);
|
|
388
|
+
recordSkip(stats, 'skippedWalkError', relPath);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (taggedCount === 0) return null;
|
|
393
|
+
|
|
394
|
+
const code = magic.toString();
|
|
395
|
+
|
|
396
|
+
if (safeMode) {
|
|
397
|
+
try {
|
|
398
|
+
const recheck = parseSFC(code, { filename: relPath });
|
|
399
|
+
if (recheck.errors.length > 0) {
|
|
400
|
+
console.warn(
|
|
401
|
+
`[harness-fe] safeMode dropped SFC injection in ${relPath} (self-check found errors)`,
|
|
402
|
+
recheck.errors,
|
|
403
|
+
);
|
|
404
|
+
recordSkip(stats, 'skippedSelfCheck', relPath);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.warn(
|
|
409
|
+
`[harness-fe] safeMode dropped SFC injection in ${relPath} (self-check threw)`,
|
|
410
|
+
err,
|
|
411
|
+
);
|
|
412
|
+
recordSkip(stats, 'skippedSelfCheck', relPath);
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (options.dryRun) {
|
|
418
|
+
if (stats) stats.elementsTagged += taggedCount;
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (stats) {
|
|
423
|
+
stats.filesInjected++;
|
|
424
|
+
stats.elementsTagged += taggedCount;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
code,
|
|
429
|
+
map: magic.generateMap({ hires: true, source: relPath, includeContent: true }),
|
|
430
|
+
taggedCount,
|
|
431
|
+
componentName,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Format the stats counter for a human-readable shutdown report. Used by
|
|
437
|
+
* the unplugin core's process-exit handler.
|
|
438
|
+
*/
|
|
439
|
+
export function formatVueTransformReport(stats: VueTransformStats): string {
|
|
440
|
+
const lines = [
|
|
441
|
+
'[harness-fe] Vue transform coverage report',
|
|
442
|
+
` files attempted: ${stats.filesAttempted}`,
|
|
443
|
+
` files injected: ${stats.filesInjected}`,
|
|
444
|
+
` elements tagged: ${stats.elementsTagged}`,
|
|
445
|
+
` skipped (SFC error): ${stats.skippedSfcError}`,
|
|
446
|
+
` skipped (template): ${stats.skippedTemplateError}`,
|
|
447
|
+
` skipped (walk error): ${stats.skippedWalkError}`,
|
|
448
|
+
` skipped (self-check): ${stats.skippedSelfCheck}`,
|
|
449
|
+
];
|
|
450
|
+
if (stats.skippedPaths.length > 0) {
|
|
451
|
+
lines.push(` first ${Math.min(stats.skippedPaths.length, 20)} skipped paths:`);
|
|
452
|
+
for (const p of stats.skippedPaths.slice(0, 20)) lines.push(` ${p}`);
|
|
453
|
+
}
|
|
454
|
+
return lines.join('\n');
|
|
455
|
+
}
|