@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/core.d.ts +19 -0
  4. package/dist/core.js +211 -0
  5. package/dist/esbuild.d.ts +9 -0
  6. package/dist/esbuild.js +9 -0
  7. package/dist/index.d.ts +20 -0
  8. package/dist/index.js +20 -0
  9. package/dist/internal/buildIdentity.d.ts +19 -0
  10. package/dist/internal/buildIdentity.js +49 -0
  11. package/dist/internal/log-capture.d.ts +7 -0
  12. package/dist/internal/log-capture.js +22 -0
  13. package/dist/internal/mcp-client.d.ts +11 -0
  14. package/dist/internal/mcp-client.js +165 -0
  15. package/dist/internal/types.d.ts +61 -0
  16. package/dist/internal/types.js +4 -0
  17. package/dist/resolveBuildId.d.ts +32 -0
  18. package/dist/resolveBuildId.js +88 -0
  19. package/dist/resolveProjectId.d.ts +9 -0
  20. package/dist/resolveProjectId.js +44 -0
  21. package/dist/rollup.d.ts +9 -0
  22. package/dist/rollup.js +9 -0
  23. package/dist/rspack.d.ts +9 -0
  24. package/dist/rspack.js +9 -0
  25. package/dist/transform.d.ts +27 -0
  26. package/dist/transform.js +150 -0
  27. package/dist/vite.d.ts +10 -0
  28. package/dist/vite.js +10 -0
  29. package/dist/vue-transform.d.ts +90 -0
  30. package/dist/vue-transform.js +350 -0
  31. package/package.json +75 -0
  32. package/src/core.ts +230 -0
  33. package/src/esbuild.ts +12 -0
  34. package/src/index.ts +34 -0
  35. package/src/internal/buildIdentity.ts +66 -0
  36. package/src/internal/log-capture.ts +26 -0
  37. package/src/internal/mcp-client.ts +181 -0
  38. package/src/internal/types.ts +66 -0
  39. package/src/resolveBuildId.test.ts +63 -0
  40. package/src/resolveBuildId.ts +125 -0
  41. package/src/resolveProjectId.test.ts +99 -0
  42. package/src/resolveProjectId.ts +48 -0
  43. package/src/rollup.ts +12 -0
  44. package/src/rspack.ts +12 -0
  45. package/src/transform.test.ts +89 -0
  46. package/src/transform.ts +188 -0
  47. package/src/vite.ts +13 -0
  48. package/src/vue-transform.test.ts +398 -0
  49. package/src/vue-transform.ts +455 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared types used by both the unplugin core and the native webpack plugin.
3
+ */
4
+ import type { ComponentMap } from '../transform.js';
5
+ export type PeerRole = 'vite-plugin' | 'webpack-plugin';
6
+ export interface HarnessFEOptions {
7
+ /** Override projectId (defaults to package.json `name`). */
8
+ projectId?: string;
9
+ /** MCP server WebSocket URL (default: ws://127.0.0.1:47729). */
10
+ mcpUrl?: string;
11
+ /** Disable injection entirely. */
12
+ disabled?: boolean;
13
+ /**
14
+ * Parent project's id, used to build the project tree on the daemon.
15
+ * Set this on the iframe child app's plugin config when you can declare
16
+ * the relationship at build time. Otherwise the runtime client will
17
+ * auto-detect it via same-origin parent inspection.
18
+ */
19
+ parentProjectId?: string;
20
+ /** Human-readable name; defaults to package.json `name`. */
21
+ displayName?: string;
22
+ /**
23
+ * Override buildId. When omitted, the plugin resolves it from git sha
24
+ * (or CI env vars) and falls back to a dev-stable hash of config files.
25
+ */
26
+ buildId?: string;
27
+ /**
28
+ * Token to authenticate against the daemon when it's bound to a non-
29
+ * loopback host. Appended as `?token=…` to the WS URL and propagated
30
+ * to the runtime client via `__HARNESS_FE__`. Read from
31
+ * `HARNESS_FE_TOKEN` when omitted.
32
+ */
33
+ token?: string;
34
+ /**
35
+ * Vue SFC transform safety: when true (default), the plugin re-parses
36
+ * its own output to catch any mis-aligned attribute injection.
37
+ */
38
+ safeMode?: boolean;
39
+ }
40
+ /**
41
+ * Context handed to the MCP client. Getters keep the values fresh as the
42
+ * host (vite/webpack) resolves them lazily (e.g. projectRoot is only known
43
+ * after configResolved / afterEnvironment).
44
+ */
45
+ export interface McpClientContext {
46
+ readonly projectId: string;
47
+ readonly mcpUrl: string;
48
+ readonly token: string | undefined;
49
+ readonly peerRole: PeerRole;
50
+ readonly parentProjectId: string | undefined;
51
+ readonly projectRoot: string;
52
+ readonly componentMap: ComponentMap;
53
+ getBuildId(): string;
54
+ getDisplayName(): string | undefined;
55
+ }
56
+ export interface McpClient {
57
+ connect(): void;
58
+ disconnect(): void;
59
+ emitEvent(name: string, payload: unknown): void;
60
+ readonly isActive: boolean;
61
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared types used by both the unplugin core and the native webpack plugin.
3
+ */
4
+ export {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves the buildId for one harness-fe build (vite dev server start /
3
+ * webpack build / prod build). One buildId = one source-code snapshot.
4
+ *
5
+ * Stability rules:
6
+ * - HMR / file edits within a dev server run → same buildId
7
+ * - dev server restart → new buildId
8
+ * - prod build → buildId matches git sha (or dirty-marked)
9
+ *
10
+ * Priority:
11
+ * 1. `userConfig` — caller-supplied via `harnessFE({ buildId })`
12
+ * 2. CI env vars (GITHUB_SHA / GIT_COMMIT) when present
13
+ * 3. `git rev-parse HEAD` + dirty marker when in a git repo
14
+ * 4. Fallback: `dev-<short-source-hash>-<startTs>` derived from package.json,
15
+ * lockfile, and bundler config — stable for the lifetime of this process.
16
+ */
17
+ export interface ResolveBuildIdOptions {
18
+ /** Caller override; wins over all auto-detection. */
19
+ userConfig?: string;
20
+ /** Project root (where package.json lives). */
21
+ root: string;
22
+ /** Stable timestamp to embed in the dev fallback id. Pass `Date.now()`
23
+ * once at plugin init so this id doesn't change across resolve() calls. */
24
+ startTs?: number;
25
+ }
26
+ export interface ResolvedBuildId {
27
+ buildId: string;
28
+ gitSha?: string;
29
+ gitDirty?: boolean;
30
+ sourceDigest?: string;
31
+ }
32
+ export declare function resolveBuildId(opts: ResolveBuildIdOptions): ResolvedBuildId;
@@ -0,0 +1,88 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ export function resolveBuildId(opts) {
6
+ if (opts.userConfig && opts.userConfig.length > 0) {
7
+ return { buildId: opts.userConfig };
8
+ }
9
+ // CI env vars take precedence over git so docker/CI builds with shallow
10
+ // checkouts still get a meaningful id even when git isn't available.
11
+ const ciSha = process.env.GITHUB_SHA ||
12
+ process.env.GIT_COMMIT ||
13
+ process.env.CI_COMMIT_SHA ||
14
+ process.env.BUILDKITE_COMMIT ||
15
+ undefined;
16
+ if (ciSha) {
17
+ return {
18
+ buildId: `${ciSha.slice(0, 12)}-ci`,
19
+ gitSha: ciSha,
20
+ gitDirty: false,
21
+ };
22
+ }
23
+ // Try git locally.
24
+ const gitSha = runGit(['rev-parse', 'HEAD'], opts.root);
25
+ if (gitSha) {
26
+ const dirty = runGit(['status', '--porcelain'], opts.root);
27
+ const gitDirty = dirty !== undefined && dirty.length > 0;
28
+ return {
29
+ buildId: `${gitSha.slice(0, 12)}${gitDirty ? '-dirty' : ''}`,
30
+ gitSha,
31
+ gitDirty,
32
+ };
33
+ }
34
+ // No git, no CI: derive a stable digest from the project's config files
35
+ // and stamp it with the dev-server start timestamp.
36
+ const startTs = opts.startTs ?? Date.now();
37
+ const sourceDigest = hashConfigFiles(opts.root);
38
+ return {
39
+ buildId: `dev-${sourceDigest.slice(0, 8)}-${startTs.toString(36)}`,
40
+ sourceDigest,
41
+ };
42
+ }
43
+ function runGit(args, cwd) {
44
+ try {
45
+ const out = spawnSync('git', args, {
46
+ cwd,
47
+ encoding: 'utf-8',
48
+ stdio: ['ignore', 'pipe', 'ignore'],
49
+ timeout: 1500,
50
+ });
51
+ if (out.status !== 0)
52
+ return undefined;
53
+ return out.stdout.trim();
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ /**
60
+ * Hash the contents of the few config files that, when changed, mean a fresh
61
+ * build artifact. Avoids reading every source file — that work belongs to the
62
+ * bundler's own incremental cache.
63
+ */
64
+ function hashConfigFiles(root) {
65
+ const candidates = [
66
+ 'package.json',
67
+ 'pnpm-lock.yaml',
68
+ 'package-lock.json',
69
+ 'yarn.lock',
70
+ 'vite.config.ts',
71
+ 'vite.config.js',
72
+ 'webpack.config.ts',
73
+ 'webpack.config.js',
74
+ 'webpack.config.cjs',
75
+ 'tsconfig.json',
76
+ ];
77
+ const h = createHash('sha256');
78
+ for (const name of candidates) {
79
+ try {
80
+ h.update(name);
81
+ h.update(readFileSync(join(root, name)));
82
+ }
83
+ catch {
84
+ // missing file → contribute the name only (still affects hash)
85
+ }
86
+ }
87
+ return h.digest('hex');
88
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Resolves the project ID for a given project root directory.
3
+ *
4
+ * Priority:
5
+ * 1. `userConfig` — if provided, return immediately without touching `.harness-id`
6
+ * 2. Read `{root}/.harness-id` — if readable, return trimmed content
7
+ * 3. Generate UUID v4, write to `{root}/.harness-id` (UTF-8, no BOM, no trailing whitespace), return it
8
+ */
9
+ export declare function resolveProjectId(root: string, userConfig?: string): Promise<string>;
@@ -0,0 +1,44 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ const HARNESS_ID_FILE = '.harness-id';
5
+ /**
6
+ * Resolves the project ID for a given project root directory.
7
+ *
8
+ * Priority:
9
+ * 1. `userConfig` — if provided, return immediately without touching `.harness-id`
10
+ * 2. Read `{root}/.harness-id` — if readable, return trimmed content
11
+ * 3. Generate UUID v4, write to `{root}/.harness-id` (UTF-8, no BOM, no trailing whitespace), return it
12
+ */
13
+ export async function resolveProjectId(root, userConfig) {
14
+ // Priority 1: explicit user config value
15
+ if (userConfig !== undefined && userConfig !== '') {
16
+ return userConfig;
17
+ }
18
+ const idFilePath = join(root, HARNESS_ID_FILE);
19
+ // Priority 2: read existing .harness-id file
20
+ try {
21
+ const content = await readFile(idFilePath, 'utf-8');
22
+ const trimmed = content.trim();
23
+ if (trimmed.length > 0) {
24
+ return trimmed;
25
+ }
26
+ }
27
+ catch (err) {
28
+ // ENOENT or other read error — fall through to generate a new UUID
29
+ const code = err.code;
30
+ if (code !== 'ENOENT') {
31
+ // Log unexpected errors but still proceed to generate a new ID
32
+ console.warn(`[harness] Could not read ${idFilePath}: ${err.message}`);
33
+ }
34
+ }
35
+ // Priority 3: generate UUID v4, write to .harness-id, return it
36
+ const newId = randomUUID();
37
+ try {
38
+ await writeFile(idFilePath, newId, { encoding: 'utf-8' });
39
+ }
40
+ catch (err) {
41
+ console.warn(`[harness] Could not write ${idFilePath}: ${err.message}`);
42
+ }
43
+ return newId;
44
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Rollup-specific export.
3
+ *
4
+ * Usage:
5
+ * import { harnessFE } from '@harness-fe/unplugin/rollup'
6
+ */
7
+ export type { HarnessFEOptions } from './core.js';
8
+ export declare const harnessFE: (options?: import("./core.js").HarnessFEOptions | undefined) => import("rollup").Plugin<any> | import("rollup").Plugin<any>[];
9
+ export default harnessFE;
package/dist/rollup.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Rollup-specific export.
3
+ *
4
+ * Usage:
5
+ * import { harnessFE } from '@harness-fe/unplugin/rollup'
6
+ */
7
+ import { unplugin } from './core.js';
8
+ export const harnessFE = unplugin.rollup;
9
+ export default harnessFE;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Rspack-specific export.
3
+ *
4
+ * Usage:
5
+ * import { harnessFE } from '@harness-fe/unplugin/rspack'
6
+ */
7
+ export type { HarnessFEOptions } from './core.js';
8
+ export declare const harnessFE: (options?: import("./core.js").HarnessFEOptions | undefined) => RspackPluginInstance;
9
+ export default harnessFE;
package/dist/rspack.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Rspack-specific export.
3
+ *
4
+ * Usage:
5
+ * import { harnessFE } from '@harness-fe/unplugin/rspack'
6
+ */
7
+ import { unplugin } from './core.js';
8
+ export const harnessFE = unplugin.rspack;
9
+ export default harnessFE;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * JSX transform: parse a .tsx / .jsx file and inject:
3
+ * - `data-morphix-loc="<relPath>:<line>:<col>"` on every JSX opening element
4
+ * - `data-morphix-comp="<ComponentName>"` on JSX opening elements that are
5
+ * enclosed (transitively) by a top-level component definition. The
6
+ * component name comes from the nearest enclosing function/class/variable
7
+ * declaration whose name starts with an uppercase letter (PascalCase).
8
+ *
9
+ * Output is the original source with the attribute strings spliced in via
10
+ * MagicString — keeps source maps intact and avoids re-generating the file.
11
+ *
12
+ * Side effect: every successfully scanned file contributes entries to the
13
+ * supplied `componentMap`: name → list of locations (file:line:col).
14
+ */
15
+ export interface ComponentLocation {
16
+ file: string;
17
+ line: number;
18
+ col: number;
19
+ }
20
+ export type ComponentMap = Map<string, ComponentLocation[]>;
21
+ export interface TransformResult {
22
+ code: string;
23
+ map?: object;
24
+ /** Number of JSX elements that got attributes. */
25
+ taggedCount: number;
26
+ }
27
+ export declare function transformJsx(source: string, relPath: string, componentMap: ComponentMap): TransformResult | null;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * JSX transform: parse a .tsx / .jsx file and inject:
3
+ * - `data-morphix-loc="<relPath>:<line>:<col>"` on every JSX opening element
4
+ * - `data-morphix-comp="<ComponentName>"` on JSX opening elements that are
5
+ * enclosed (transitively) by a top-level component definition. The
6
+ * component name comes from the nearest enclosing function/class/variable
7
+ * declaration whose name starts with an uppercase letter (PascalCase).
8
+ *
9
+ * Output is the original source with the attribute strings spliced in via
10
+ * MagicString — keeps source maps intact and avoids re-generating the file.
11
+ *
12
+ * Side effect: every successfully scanned file contributes entries to the
13
+ * supplied `componentMap`: name → list of locations (file:line:col).
14
+ */
15
+ import { parse } from '@babel/parser';
16
+ import traverseDefault from '@babel/traverse';
17
+ import * as t from '@babel/types';
18
+ import MagicString from 'magic-string';
19
+ // @babel/traverse exposes its default as `default` only when published as ESM
20
+ // in older versions; later versions also expose a named export. Pick whichever
21
+ // is callable.
22
+ const traverse = typeof traverseDefault === 'function'
23
+ ? traverseDefault
24
+ : (traverseDefault.default ?? traverseDefault);
25
+ const ATTR_COMP = 'data-morphix-comp';
26
+ const ATTR_LOC = 'data-morphix-loc';
27
+ const PASCAL_CASE = /^[A-Z][A-Za-z0-9]*$/;
28
+ export function transformJsx(source, relPath, componentMap) {
29
+ let ast;
30
+ try {
31
+ ast = parse(source, {
32
+ sourceType: 'module',
33
+ plugins: ['jsx', 'typescript'],
34
+ errorRecovery: true,
35
+ });
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ const magic = new MagicString(source);
41
+ let taggedCount = 0;
42
+ traverse(ast, {
43
+ JSXOpeningElement(path) {
44
+ const node = path.node;
45
+ const start = node.start;
46
+ if (start == null)
47
+ return;
48
+ // Position attribute insertion right after the tag name token.
49
+ const name = node.name;
50
+ const nameEnd = name.end;
51
+ if (nameEnd == null)
52
+ return;
53
+ const loc = node.loc?.start;
54
+ if (!loc)
55
+ return;
56
+ const locValue = `${relPath}:${loc.line}:${loc.column}`;
57
+ const enclosingName = findEnclosingComponentName(path);
58
+ const explicitName = getStringAttribute(node, ATTR_COMP);
59
+ const attrs = [];
60
+ if (!hasAttribute(node, ATTR_LOC)) {
61
+ attrs.push(`${ATTR_LOC}="${escapeAttr(locValue)}"`);
62
+ }
63
+ if (!hasAttribute(node, ATTR_COMP) && enclosingName) {
64
+ attrs.push(`${ATTR_COMP}="${escapeAttr(enclosingName)}"`);
65
+ }
66
+ // Register every name that ends up on this element into the map —
67
+ // both enclosing component (so e.g. App resolves) and any
68
+ // hand-written tag (so IncrementBtn / EchoInput resolve too).
69
+ const names = new Set();
70
+ if (enclosingName)
71
+ names.add(enclosingName);
72
+ if (explicitName)
73
+ names.add(explicitName);
74
+ for (const name of names) {
75
+ const entries = componentMap.get(name) ?? [];
76
+ entries.push({ file: relPath, line: loc.line, col: loc.column });
77
+ componentMap.set(name, entries);
78
+ }
79
+ if (!attrs.length)
80
+ return;
81
+ // Insert: <Foo ATTRS … >
82
+ magic.appendLeft(nameEnd, ' ' + attrs.join(' '));
83
+ taggedCount++;
84
+ },
85
+ });
86
+ if (taggedCount === 0)
87
+ return null;
88
+ return {
89
+ code: magic.toString(),
90
+ map: magic.generateMap({ hires: true, source: relPath, includeContent: true }),
91
+ taggedCount,
92
+ };
93
+ }
94
+ function getStringAttribute(node, name) {
95
+ for (const attr of node.attributes) {
96
+ if (attr.type === 'JSXAttribute' &&
97
+ attr.name.type === 'JSXIdentifier' &&
98
+ attr.name.name === name &&
99
+ attr.value &&
100
+ attr.value.type === 'StringLiteral') {
101
+ return attr.value.value;
102
+ }
103
+ }
104
+ return undefined;
105
+ }
106
+ function hasAttribute(node, name) {
107
+ for (const attr of node.attributes) {
108
+ if (attr.type === 'JSXAttribute' &&
109
+ attr.name.type === 'JSXIdentifier' &&
110
+ attr.name.name === name) {
111
+ return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ function findEnclosingComponentName(path) {
117
+ let current = path.parentPath;
118
+ while (current) {
119
+ const node = current.node;
120
+ // function Foo() { return <jsx/> }
121
+ if (t.isFunctionDeclaration(node) && node.id && PASCAL_CASE.test(node.id.name)) {
122
+ return node.id.name;
123
+ }
124
+ // const Foo = (...) => <jsx/>
125
+ // const Foo = function (...) { return <jsx/> }
126
+ if ((t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) &&
127
+ current.parentPath &&
128
+ t.isVariableDeclarator(current.parentPath.node) &&
129
+ t.isIdentifier(current.parentPath.node.id) &&
130
+ PASCAL_CASE.test(current.parentPath.node.id.name)) {
131
+ return current.parentPath.node.id.name;
132
+ }
133
+ // class Foo extends Component { render() { return <jsx/> } }
134
+ if (t.isClassDeclaration(node) && node.id && PASCAL_CASE.test(node.id.name)) {
135
+ return node.id.name;
136
+ }
137
+ // export default function Foo() ...
138
+ if (t.isExportDefaultDeclaration(node) &&
139
+ t.isFunctionDeclaration(node.declaration) &&
140
+ node.declaration.id &&
141
+ PASCAL_CASE.test(node.declaration.id.name)) {
142
+ return node.declaration.id.name;
143
+ }
144
+ current = current.parentPath;
145
+ }
146
+ return undefined;
147
+ }
148
+ function escapeAttr(value) {
149
+ return value.replace(/"/g, '&quot;');
150
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Vite-specific export.
3
+ *
4
+ * Usage:
5
+ * import { harnessFE } from '@harness-fe/unplugin/vite'
6
+ * export default defineConfig({ plugins: [harnessFE()] })
7
+ */
8
+ export type { HarnessFEOptions } from './core.js';
9
+ export declare const harnessFE: (options?: import("./core.js").HarnessFEOptions | undefined) => import("unplugin").VitePlugin<any> | import("unplugin").VitePlugin<any>[];
10
+ export default harnessFE;
package/dist/vite.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Vite-specific export.
3
+ *
4
+ * Usage:
5
+ * import { harnessFE } from '@harness-fe/unplugin/vite'
6
+ * export default defineConfig({ plugins: [harnessFE()] })
7
+ */
8
+ import { unplugin } from './core.js';
9
+ export const harnessFE = unplugin.vite;
10
+ export default harnessFE;
@@ -0,0 +1,90 @@
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
+ import type { ComponentMap } from './transform.js';
14
+ export interface VueTransformResult {
15
+ code: string;
16
+ map?: object;
17
+ taggedCount: number;
18
+ componentName: string | undefined;
19
+ }
20
+ /**
21
+ * Counters maintained across calls — populated even in dry-run mode. The
22
+ * unplugin core attaches a single instance per dev-server lifetime and
23
+ * dumps it on process exit so users can see how many Vue 2-era files were
24
+ * skipped (filter syntax, functional templates, malformed offsets, …).
25
+ */
26
+ export interface VueTransformStats {
27
+ filesAttempted: number;
28
+ filesInjected: number;
29
+ elementsTagged: number;
30
+ skippedSfcError: number;
31
+ skippedTemplateError: number;
32
+ skippedWalkError: number;
33
+ skippedSelfCheck: number;
34
+ /** Sample of skipped file paths (capped at 50 to bound memory). */
35
+ skippedPaths: string[];
36
+ }
37
+ export declare function createVueTransformStats(): VueTransformStats;
38
+ export interface VueTransformOptions {
39
+ /**
40
+ * When true (default), the transform re-parses its own output before
41
+ * returning it. Catches MagicString offset bugs against malformed Vue
42
+ * 2-era syntax before vue-loader ever sees them.
43
+ */
44
+ safeMode?: boolean;
45
+ /**
46
+ * When true, walk the AST and populate the componentMap as usual, but
47
+ * always return null (no source injection). Used by the dry-run
48
+ * coverage report.
49
+ */
50
+ dryRun?: boolean;
51
+ /** Counters to update; ignored if omitted. */
52
+ stats?: VueTransformStats;
53
+ }
54
+ /**
55
+ * Inject `data-morphix-*` attributes into a raw Vue template HTML fragment.
56
+ *
57
+ * Used by the webpack pipeline to handle the `*.vue?vue&type=template` virtual
58
+ * sub-module emitted by vue-loader. vue-loader's `templateLoader` will then
59
+ * compile the (now-tagged) template into a render function, preserving the
60
+ * attributes on every element vnode.
61
+ *
62
+ * `lineOffset` is added to every element's reported line number — pass the
63
+ * 1-based line index where this template appears in the original `.vue` file
64
+ * (so locations remain file-relative, not template-relative).
65
+ */
66
+ export declare function transformVueTemplate(templateSource: string, relPath: string, componentName: string | undefined, componentMap: ComponentMap, lineOffset?: number, options?: VueTransformOptions): {
67
+ code: string;
68
+ map?: object;
69
+ taggedCount: number;
70
+ } | null;
71
+ /**
72
+ * Resolve the component name from a raw .vue source (used by webpack pipeline
73
+ * where we only see the template sub-module and need to look up the parent's
74
+ * component name from disk).
75
+ */
76
+ export declare function resolveVueComponentName(source: string, relPath: string): string | undefined;
77
+ /**
78
+ * Compute the 0-based line offset where the `<template>` *content* begins in
79
+ * the original .vue file. Adding this to template-relative line numbers gives
80
+ * file-relative numbers suitable for `data-morphix-loc`.
81
+ *
82
+ * Returns 0 if the SFC cannot be parsed or has no template block.
83
+ */
84
+ export declare function getTemplateLineOffset(source: string, relPath: string): number;
85
+ export declare function transformVueSFC(source: string, relPath: string, componentMap: ComponentMap, options?: VueTransformOptions): VueTransformResult | null;
86
+ /**
87
+ * Format the stats counter for a human-readable shutdown report. Used by
88
+ * the unplugin core's process-exit handler.
89
+ */
90
+ export declare function formatVueTransformReport(stats: VueTransformStats): string;