@harness-fe/unplugin 3.2.0 → 4.0.0-next.11
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 +1 -1
- package/dist/core.js +17 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/types.d.ts +13 -0
- package/dist/soloTarget.d.ts +11 -0
- package/dist/soloTarget.js +23 -0
- package/package.json +11 -8
- package/src/core.ts +17 -3
- package/src/index.ts +1 -0
- package/src/internal/types.ts +13 -0
- package/src/soloTarget.test.ts +29 -0
- package/src/soloTarget.ts +26 -0
package/LICENSE
CHANGED
package/dist/core.js
CHANGED
|
@@ -20,6 +20,7 @@ import { DEFAULT_WS_PORT } from '@harness-fe/protocol';
|
|
|
20
20
|
import { transformJsx } from './transform.js';
|
|
21
21
|
import { transformVueSFC, transformVueTemplate, resolveVueComponentName, getTemplateLineOffset, createVueTransformStats, formatVueTransformReport, } from './vue-transform.js';
|
|
22
22
|
import { resolveProjectId } from './resolveProjectId.js';
|
|
23
|
+
import { resolveSoloTarget } from './soloTarget.js';
|
|
23
24
|
import { createMcpClient } from './internal/mcp-client.js';
|
|
24
25
|
import { installNodeLogCapture } from './internal/log-capture.js';
|
|
25
26
|
import { appendTokenQuery, createBuildIdentity } from './internal/buildIdentity.js';
|
|
@@ -31,7 +32,7 @@ const VIRTUAL_RUNTIME_ID = 'virtual:harness-fe/runtime';
|
|
|
31
32
|
const RESOLVED_VIRTUAL_RUNTIME_ID = '\0' + VIRTUAL_RUNTIME_ID;
|
|
32
33
|
export const unpluginFactory = (options = {}) => {
|
|
33
34
|
let projectId = options.projectId ?? 'unknown-project';
|
|
34
|
-
const baseMcpUrl = options.mcpUrl ?? process.env.HARNESS_FE_URL ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
35
|
+
const baseMcpUrl = options.mcpUrl ?? process.env.HARNESS_FE_URL ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
35
36
|
const token = options.token ?? process.env.HARNESS_FE_TOKEN;
|
|
36
37
|
const mcpUrl = appendTokenQuery(baseMcpUrl, token);
|
|
37
38
|
let projectRoot = process.cwd();
|
|
@@ -159,10 +160,23 @@ export const unpluginFactory = (options = {}) => {
|
|
|
159
160
|
projectRoot = config.root ?? process.cwd();
|
|
160
161
|
projectId = await resolveProjectId(projectRoot, options.projectId);
|
|
161
162
|
},
|
|
162
|
-
configureServer(server) {
|
|
163
|
+
async configureServer(server) {
|
|
163
164
|
if (options.disabled)
|
|
164
165
|
return;
|
|
165
166
|
const client = ensureMcpClient();
|
|
167
|
+
// Solo: make sure a shared local gateway is up before connecting.
|
|
168
|
+
// Best-effort — if @harness-fe/cli isn't installed or the gateway
|
|
169
|
+
// is slow, the client still retries on its own.
|
|
170
|
+
const solo = resolveSoloTarget(baseMcpUrl, Boolean(token));
|
|
171
|
+
if (solo) {
|
|
172
|
+
try {
|
|
173
|
+
const { ensureSharedGateway } = await import('@harness-fe/cli/sharedGateway');
|
|
174
|
+
await ensureSharedGateway({ host: solo.host, port: solo.port });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* keep going; client.connect() retries */
|
|
178
|
+
}
|
|
179
|
+
}
|
|
166
180
|
client.connect();
|
|
167
181
|
logCaptureCleanup = installNodeLogCapture((name, payload) => client.emitEvent(name, payload));
|
|
168
182
|
server.httpServer?.once('close', () => {
|
|
@@ -189,7 +203,7 @@ export const unpluginFactory = (options = {}) => {
|
|
|
189
203
|
return html;
|
|
190
204
|
const injection = `<!-- @harness-fe injected (dev only) -->
|
|
191
205
|
<script>
|
|
192
|
-
window.__HARNESS_FE__ = ${JSON.stringify({ projectId, mcpUrl, buildId: identity.getBuildId(projectRoot), parentProjectId: options.parentProjectId, displayName: identity.getDisplayName(projectRoot) })};
|
|
206
|
+
window.__HARNESS_FE__ = ${JSON.stringify({ projectId, mcpUrl, buildId: identity.getBuildId(projectRoot), parentProjectId: options.parentProjectId, displayName: identity.getDisplayName(projectRoot), overlay: options.overlay ?? true, consent: options.consent })};
|
|
193
207
|
</script>
|
|
194
208
|
<script type="module">import '${VIRTUAL_RUNTIME_ID}';</script>`;
|
|
195
209
|
return html.replace(/<\/head>/i, `${injection}\n</head>`);
|
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,6 @@ export { transformVueSFC, transformVueTemplate, resolveVueComponentName, getTemp
|
|
|
16
16
|
export { createMcpClient } from './internal/mcp-client.js';
|
|
17
17
|
export { installNodeLogCapture } from './internal/log-capture.js';
|
|
18
18
|
export { createBuildIdentity, appendTokenQuery } from './internal/buildIdentity.js';
|
|
19
|
+
export { resolveSoloTarget } from './soloTarget.js';
|
|
19
20
|
export type { McpClient, McpClientContext, PeerRole } from './internal/types.js';
|
|
20
21
|
export type { BuildIdentity, BuildIdentityOptions } from './internal/buildIdentity.js';
|
package/dist/index.js
CHANGED
|
@@ -18,3 +18,4 @@ export { transformVueSFC, transformVueTemplate, resolveVueComponentName, getTemp
|
|
|
18
18
|
export { createMcpClient } from './internal/mcp-client.js';
|
|
19
19
|
export { installNodeLogCapture } from './internal/log-capture.js';
|
|
20
20
|
export { createBuildIdentity, appendTokenQuery } from './internal/buildIdentity.js';
|
|
21
|
+
export { resolveSoloTarget } from './soloTarget.js';
|
package/dist/internal/types.d.ts
CHANGED
|
@@ -36,6 +36,19 @@ export interface HarnessFEOptions {
|
|
|
36
36
|
* its own output to catch any mis-aligned attribute injection.
|
|
37
37
|
*/
|
|
38
38
|
safeMode?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Show the in-page "H" overlay (default: true). Set to false to hide the
|
|
41
|
+
* overlay in production dogfood scenarios — data capture is unaffected.
|
|
42
|
+
*/
|
|
43
|
+
overlay?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Browser consent policy. When set, takes priority over the gateway
|
|
46
|
+
* hello.ack consent mode.
|
|
47
|
+
* 'off' — no user prompt, control commands run freely (default)
|
|
48
|
+
* 'session' — user grants once per page-load
|
|
49
|
+
* 'always' — prompt before every control command
|
|
50
|
+
*/
|
|
51
|
+
consent?: 'off' | 'session' | 'always';
|
|
39
52
|
}
|
|
40
53
|
/**
|
|
41
54
|
* Context handed to the MCP client. Getters keep the values fresh as the
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether this build is "solo" (a loopback target, no token) and, if so,
|
|
3
|
+
* the (host, port) on which the dev server should auto-spawn / reuse a shared
|
|
4
|
+
* gateway. Returns null for "team" (an explicit token, or a non-loopback /
|
|
5
|
+
* unparseable target) — in which case the plugin must NOT spawn anything; that
|
|
6
|
+
* gateway is deployed and owned elsewhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveSoloTarget(mcpUrl: string, hasToken: boolean): {
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
} | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DEFAULT_WS_PORT } from '@harness-fe/protocol';
|
|
2
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
3
|
+
/**
|
|
4
|
+
* Decide whether this build is "solo" (a loopback target, no token) and, if so,
|
|
5
|
+
* the (host, port) on which the dev server should auto-spawn / reuse a shared
|
|
6
|
+
* gateway. Returns null for "team" (an explicit token, or a non-loopback /
|
|
7
|
+
* unparseable target) — in which case the plugin must NOT spawn anything; that
|
|
8
|
+
* gateway is deployed and owned elsewhere.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveSoloTarget(mcpUrl, hasToken) {
|
|
11
|
+
if (hasToken)
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(mcpUrl);
|
|
15
|
+
if (LOOPBACK_HOSTS.has(u.hostname)) {
|
|
16
|
+
return { host: u.hostname, port: u.port ? Number(u.port) : DEFAULT_WS_PORT };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* unparseable URL → treat as team (don't spawn) */
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-fe/unplugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-next.11",
|
|
4
4
|
"description": "Unified build plugin for Harness-FE. Supports Vite, Rspack, esbuild, and Rollup via unplugin. Webpack users should use @harness-fe/webpack (native plugin) instead.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,15 +44,18 @@
|
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@babel/parser": "^7.29.
|
|
48
|
-
"@babel/traverse": "^7.29.
|
|
49
|
-
"@babel/types": "^7.29.
|
|
50
|
-
"@vue/compiler-dom": "^3.
|
|
51
|
-
"@vue/compiler-sfc": "^3.
|
|
47
|
+
"@babel/parser": "^7.29.7",
|
|
48
|
+
"@babel/traverse": "^7.29.7",
|
|
49
|
+
"@babel/types": "^7.29.7",
|
|
50
|
+
"@vue/compiler-dom": "^3.5.35",
|
|
51
|
+
"@vue/compiler-sfc": "^3.5.35",
|
|
52
52
|
"magic-string": "^0.30.21",
|
|
53
53
|
"unplugin": "^2.3.2",
|
|
54
|
-
"ws": "^8.
|
|
55
|
-
"@harness-fe/protocol": "
|
|
54
|
+
"ws": "^8.21.0",
|
|
55
|
+
"@harness-fe/protocol": "4.0.0-next.8"
|
|
56
|
+
},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"@harness-fe/cli": "4.0.0-next.11"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@types/babel__traverse": "^7.28.0",
|
package/src/core.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type VueTransformOptions,
|
|
31
31
|
} from './vue-transform.js';
|
|
32
32
|
import { resolveProjectId } from './resolveProjectId.js';
|
|
33
|
+
import { resolveSoloTarget } from './soloTarget.js';
|
|
33
34
|
import { createMcpClient } from './internal/mcp-client.js';
|
|
34
35
|
import { installNodeLogCapture } from './internal/log-capture.js';
|
|
35
36
|
import { appendTokenQuery, createBuildIdentity } from './internal/buildIdentity.js';
|
|
@@ -48,7 +49,7 @@ export type { HarnessFEOptions };
|
|
|
48
49
|
export const unpluginFactory: UnpluginFactory<HarnessFEOptions | undefined> = (options = {}) => {
|
|
49
50
|
let projectId = options.projectId ?? 'unknown-project';
|
|
50
51
|
const baseMcpUrl =
|
|
51
|
-
options.mcpUrl ?? process.env.HARNESS_FE_URL ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
52
|
+
options.mcpUrl ?? process.env.HARNESS_FE_URL ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
52
53
|
const token = options.token ?? process.env.HARNESS_FE_TOKEN;
|
|
53
54
|
const mcpUrl = appendTokenQuery(baseMcpUrl, token);
|
|
54
55
|
let projectRoot = process.cwd();
|
|
@@ -104,6 +105,7 @@ export const unpluginFactory: UnpluginFactory<HarnessFEOptions | undefined> = (o
|
|
|
104
105
|
return mcpClient;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
|
|
107
109
|
// Expose for advanced usage (e.g. tests or downstream plugins inspecting state).
|
|
108
110
|
const ctx = {
|
|
109
111
|
get projectId() { return projectId; },
|
|
@@ -176,9 +178,21 @@ export const unpluginFactory: UnpluginFactory<HarnessFEOptions | undefined> = (o
|
|
|
176
178
|
projectId = await resolveProjectId(projectRoot, options.projectId);
|
|
177
179
|
},
|
|
178
180
|
|
|
179
|
-
configureServer(server: any) {
|
|
181
|
+
async configureServer(server: any) {
|
|
180
182
|
if (options.disabled) return;
|
|
181
183
|
const client = ensureMcpClient();
|
|
184
|
+
// Solo: make sure a shared local gateway is up before connecting.
|
|
185
|
+
// Best-effort — if @harness-fe/cli isn't installed or the gateway
|
|
186
|
+
// is slow, the client still retries on its own.
|
|
187
|
+
const solo = resolveSoloTarget(baseMcpUrl, Boolean(token));
|
|
188
|
+
if (solo) {
|
|
189
|
+
try {
|
|
190
|
+
const { ensureSharedGateway } = await import('@harness-fe/cli/sharedGateway');
|
|
191
|
+
await ensureSharedGateway({ host: solo.host, port: solo.port });
|
|
192
|
+
} catch {
|
|
193
|
+
/* keep going; client.connect() retries */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
182
196
|
client.connect();
|
|
183
197
|
logCaptureCleanup = installNodeLogCapture((name, payload) => client.emitEvent(name, payload));
|
|
184
198
|
server.httpServer?.once('close', () => {
|
|
@@ -205,7 +219,7 @@ export const unpluginFactory: UnpluginFactory<HarnessFEOptions | undefined> = (o
|
|
|
205
219
|
if (options.disabled) return html;
|
|
206
220
|
const injection = `<!-- @harness-fe injected (dev only) -->
|
|
207
221
|
<script>
|
|
208
|
-
window.__HARNESS_FE__ = ${JSON.stringify({ projectId, mcpUrl, buildId: identity.getBuildId(projectRoot), parentProjectId: options.parentProjectId, displayName: identity.getDisplayName(projectRoot) })};
|
|
222
|
+
window.__HARNESS_FE__ = ${JSON.stringify({ projectId, mcpUrl, buildId: identity.getBuildId(projectRoot), parentProjectId: options.parentProjectId, displayName: identity.getDisplayName(projectRoot), overlay: options.overlay ?? true, consent: options.consent })};
|
|
209
223
|
</script>
|
|
210
224
|
<script type="module">import '${VIRTUAL_RUNTIME_ID}';</script>`;
|
|
211
225
|
return html.replace(/<\/head>/i, `${injection}\n</head>`);
|
package/src/index.ts
CHANGED
|
@@ -30,5 +30,6 @@ export {
|
|
|
30
30
|
export { createMcpClient } from './internal/mcp-client.js';
|
|
31
31
|
export { installNodeLogCapture } from './internal/log-capture.js';
|
|
32
32
|
export { createBuildIdentity, appendTokenQuery } from './internal/buildIdentity.js';
|
|
33
|
+
export { resolveSoloTarget } from './soloTarget.js';
|
|
33
34
|
export type { McpClient, McpClientContext, PeerRole } from './internal/types.js';
|
|
34
35
|
export type { BuildIdentity, BuildIdentityOptions } from './internal/buildIdentity.js';
|
package/src/internal/types.ts
CHANGED
|
@@ -39,6 +39,19 @@ export interface HarnessFEOptions {
|
|
|
39
39
|
* its own output to catch any mis-aligned attribute injection.
|
|
40
40
|
*/
|
|
41
41
|
safeMode?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Show the in-page "H" overlay (default: true). Set to false to hide the
|
|
44
|
+
* overlay in production dogfood scenarios — data capture is unaffected.
|
|
45
|
+
*/
|
|
46
|
+
overlay?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Browser consent policy. When set, takes priority over the gateway
|
|
49
|
+
* hello.ack consent mode.
|
|
50
|
+
* 'off' — no user prompt, control commands run freely (default)
|
|
51
|
+
* 'session' — user grants once per page-load
|
|
52
|
+
* 'always' — prompt before every control command
|
|
53
|
+
*/
|
|
54
|
+
consent?: 'off' | 'session' | 'always';
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DEFAULT_WS_PORT } from '@harness-fe/protocol';
|
|
3
|
+
import { resolveSoloTarget } from './soloTarget.js';
|
|
4
|
+
|
|
5
|
+
describe('resolveSoloTarget', () => {
|
|
6
|
+
it('loopback + no token → spawn target with explicit port', () => {
|
|
7
|
+
expect(resolveSoloTarget('ws://127.0.0.1:47729/ws', false)).toEqual({ host: '127.0.0.1', port: 47729 });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('localhost host is loopback too', () => {
|
|
11
|
+
expect(resolveSoloTarget('ws://localhost:48000/ws', false)).toEqual({ host: 'localhost', port: 48000 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('no port in URL → DEFAULT_WS_PORT', () => {
|
|
15
|
+
expect(resolveSoloTarget('ws://127.0.0.1/ws', false)).toEqual({ host: '127.0.0.1', port: DEFAULT_WS_PORT });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('has token → null (team: never auto-spawn)', () => {
|
|
19
|
+
expect(resolveSoloTarget('ws://127.0.0.1:47729/ws', true)).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('non-loopback host → null (team: remote gateway owned elsewhere)', () => {
|
|
23
|
+
expect(resolveSoloTarget('ws://10.0.0.5:9000/ws', false)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('unparseable URL → null', () => {
|
|
27
|
+
expect(resolveSoloTarget('not a url', false)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DEFAULT_WS_PORT } from '@harness-fe/protocol';
|
|
2
|
+
|
|
3
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decide whether this build is "solo" (a loopback target, no token) and, if so,
|
|
7
|
+
* the (host, port) on which the dev server should auto-spawn / reuse a shared
|
|
8
|
+
* gateway. Returns null for "team" (an explicit token, or a non-loopback /
|
|
9
|
+
* unparseable target) — in which case the plugin must NOT spawn anything; that
|
|
10
|
+
* gateway is deployed and owned elsewhere.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveSoloTarget(
|
|
13
|
+
mcpUrl: string,
|
|
14
|
+
hasToken: boolean,
|
|
15
|
+
): { host: string; port: number } | null {
|
|
16
|
+
if (hasToken) return null;
|
|
17
|
+
try {
|
|
18
|
+
const u = new URL(mcpUrl);
|
|
19
|
+
if (LOOPBACK_HOSTS.has(u.hostname)) {
|
|
20
|
+
return { host: u.hostname, port: u.port ? Number(u.port) : DEFAULT_WS_PORT };
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
/* unparseable URL → treat as team (don't spawn) */
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|