@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,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,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
|
+
}
|
package/dist/rollup.d.ts
ADDED
|
@@ -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
package/dist/rspack.d.ts
ADDED
|
@@ -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,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, '"');
|
|
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;
|