@harness-fe/unplugin 4.0.0-next.4 → 4.0.0-next.6

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 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.4",
3
+ "version": "4.0.0-next.6",
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",
@@ -52,7 +52,10 @@
52
52
  "magic-string": "^0.30.21",
53
53
  "unplugin": "^2.3.2",
54
54
  "ws": "^8.18.0",
55
- "@harness-fe/protocol": "4.0.0-next.4"
55
+ "@harness-fe/protocol": "4.0.0-next.6"
56
+ },
57
+ "optionalDependencies": {
58
+ "@harness-fe/cli": "4.0.0-next.6"
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', () => {
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
+ }