@harness-fe/unplugin 4.0.0-next.4 → 4.0.0-next.5
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/dist/core.js +16 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/soloTarget.d.ts +11 -0
- package/dist/soloTarget.js +23 -0
- package/package.json +4 -1
- package/src/core.ts +16 -2
- package/src/index.ts +1 -0
- package/src/soloTarget.test.ts +29 -0
- package/src/soloTarget.ts +26 -0
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', () => {
|
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';
|
|
@@ -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": "4.0.0-next.
|
|
3
|
+
"version": "4.0.0-next.5",
|
|
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",
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
"ws": "^8.18.0",
|
|
55
55
|
"@harness-fe/protocol": "4.0.0-next.4"
|
|
56
56
|
},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"@harness-fe/cli": "4.0.0-next.5"
|
|
59
|
+
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@types/babel__traverse": "^7.28.0",
|
|
59
62
|
"@types/ws": "^8.5.10",
|
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', () => {
|
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';
|
|
@@ -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
|
+
}
|