Skip to content

Commit 3239cde

Browse files
authored
Merge pull request #196 from proofgeist/t3code/fm-bridge-warning-fallback
2 parents 242a064 + 38c126e commit 3239cde

4 files changed

Lines changed: 160 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@proofkit/webviewer": patch
3+
---
4+
5+
Soften the Vite FM bridge startup path when FM MCP responds but has no connected files. The dev server now logs a warning, injects a fallback bridge shim, and logs runtime errors if bridge calls are made before a file connects. Unreachable or unhealthy FM MCP still fails setup.

packages/webviewer/src/fm-bridge.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export const trimToNull = (value: unknown): string | null => {
2424

2525
export const normalizeBaseUrl = (value: string): string => value.replace(TRAILING_SLASH_PATTERN, "");
2626

27+
export const buildNoConnectedFilesWarning = (connectedFilesUrl: string): string =>
28+
`fmBridge found no connected FileMaker files at ${connectedFilesUrl}. Dev server will continue. Connect a FileMaker webviewer to enable bridge forwarding.`;
29+
30+
export const buildNoConnectedFilesRuntimeError = (connectedFilesUrl: string): string =>
31+
`fmBridge could not forward message because no connected FileMaker file is available from ${connectedFilesUrl}.`;
32+
2733
export const resolveWsUrl = (options: Pick<FmBridgeOptions, "fmMcpBaseUrl" | "wsUrl">): string => {
2834
const explicitWsUrl = trimToNull(options.wsUrl);
2935
if (explicitWsUrl) {
@@ -41,7 +47,7 @@ export const resolveWsUrl = (options: Pick<FmBridgeOptions, "fmMcpBaseUrl" | "ws
4147
}
4248
};
4349

44-
export const discoverConnectedFileName = async (baseUrl: string): Promise<string> => {
50+
export const discoverConnectedFileName = async (baseUrl: string): Promise<string | null> => {
4551
const connectedFilesUrl = `${normalizeBaseUrl(baseUrl)}/connectedFiles`;
4652
const controller = new AbortController();
4753
const timeoutId = setTimeout(() => {
@@ -76,9 +82,7 @@ export const discoverConnectedFileName = async (baseUrl: string): Promise<string
7682
const firstFileName = payload.find((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
7783

7884
if (!firstFileName) {
79-
throw new Error(
80-
`fmBridge found no connected FileMaker files at ${connectedFilesUrl}. Open FileMaker and load /webviewer?fileName=YourFile.`,
81-
);
85+
return null;
8286
}
8387

8488
return firstFileName;
@@ -109,6 +113,41 @@ export const buildMockScriptTag = (options: {
109113
};
110114
};
111115

116+
export const buildNoConnectedFilesScriptTag = (baseUrl: string): HtmlTagDescriptor => {
117+
const connectedFilesUrl = `${normalizeBaseUrl(baseUrl)}/connectedFiles`;
118+
const errorMessage = buildNoConnectedFilesRuntimeError(connectedFilesUrl);
119+
120+
return {
121+
tag: "script",
122+
injectTo: "head-prepend",
123+
children: `
124+
(() => {
125+
const errorMessage = ${JSON.stringify(errorMessage)};
126+
const report = () => {
127+
console.error(errorMessage);
128+
return undefined;
129+
};
130+
131+
if (!window.filemaker) {
132+
const filemakerStub = function filemaker() {
133+
return report();
134+
};
135+
filemakerStub.performScript = report;
136+
filemakerStub.performScriptWithOption = report;
137+
window.filemaker = filemakerStub;
138+
}
139+
140+
if (!window.FileMaker) {
141+
window.FileMaker = {
142+
PerformScript: report,
143+
PerformScriptWithOption: report,
144+
};
145+
}
146+
})();
147+
`.trim(),
148+
};
149+
};
150+
112151
export const fmBridge = (options: FmBridgeOptions = {}): Plugin => {
113152
const baseUrl = trimToNull(options.fmMcpBaseUrl) ?? defaultFmMcpBaseUrl;
114153
const wsUrl = resolveWsUrl(options);
@@ -132,6 +171,9 @@ export const fmBridge = (options: FmBridgeOptions = {}): Plugin => {
132171
}
133172

134173
resolvedFileName = await discoverConnectedFileName(baseUrl);
174+
if (!resolvedFileName) {
175+
console.warn(buildNoConnectedFilesWarning(`${normalizeBaseUrl(baseUrl)}/connectedFiles`));
176+
}
135177
},
136178
async transformIndexHtml() {
137179
if (!isServeMode) {
@@ -142,12 +184,14 @@ export const fmBridge = (options: FmBridgeOptions = {}): Plugin => {
142184
resolvedFileName = await discoverConnectedFileName(baseUrl);
143185
}
144186

145-
const tag = buildMockScriptTag({
146-
baseUrl,
147-
fileName: resolvedFileName,
148-
wsUrl,
149-
debug,
150-
});
187+
const tag = resolvedFileName
188+
? buildMockScriptTag({
189+
baseUrl,
190+
fileName: resolvedFileName,
191+
wsUrl,
192+
debug,
193+
})
194+
: buildNoConnectedFilesScriptTag(baseUrl);
151195

152196
return tag ? [tag] : undefined;
153197
},

packages/webviewer/src/types.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
11
interface Window {
22
handleFmWVFetchCallback: (data: unknown, fetchId: string) => boolean;
3+
FileMaker?: {
4+
PerformScript: (name: string, parameter: string) => void;
5+
PerformScriptWithOption: (name: string, parameter: string, option: "0" | "1" | "2" | "3" | "4" | "5") => void;
6+
};
7+
filemaker?: {
8+
(...args: unknown[]): unknown;
9+
performScript?: (...args: unknown[]) => unknown;
10+
performScriptWithOption?: (...args: unknown[]) => unknown;
11+
};
312
}

packages/webviewer/tests/vite-plugins.test.ts

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33

44
import {
55
buildMockScriptTag,
6+
buildNoConnectedFilesRuntimeError,
67
defaultWsUrl,
78
discoverConnectedFileName,
89
fmBridge,
@@ -130,29 +131,39 @@ describe("discoverConnectedFileName", () => {
130131
);
131132
});
132133

133-
it("rejects when no connected files are available", async () => {
134+
it("returns null when no connected files are available", async () => {
134135
vi.mocked(globalThis.fetch).mockResolvedValue(
135136
new Response(JSON.stringify(["", " "]), {
136137
status: 200,
137138
headers: { "Content-Type": "application/json" },
138139
}),
139140
);
140141

141-
await expect(discoverConnectedFileName("http://localhost:1365")).rejects.toThrow(
142-
"fmBridge found no connected FileMaker files at http://localhost:1365/connectedFiles. Open FileMaker and load /webviewer?fileName=YourFile.",
143-
);
142+
await expect(discoverConnectedFileName("http://localhost:1365")).resolves.toBeNull();
144143
});
145144
});
146145

147146
describe("fmBridge", () => {
148147
const originalFetch = globalThis.fetch;
148+
const originalConsoleWarn = console.warn;
149+
const originalConsoleError = console.error;
150+
const originalWindow = globalThis.window;
149151

150152
beforeEach(() => {
151153
globalThis.fetch = vi.fn();
154+
console.warn = vi.fn();
155+
console.error = vi.fn();
152156
});
153157

154158
afterEach(() => {
155159
globalThis.fetch = originalFetch;
160+
console.warn = originalConsoleWarn;
161+
console.error = originalConsoleError;
162+
if (typeof originalWindow === "undefined") {
163+
Reflect.deleteProperty(globalThis, "window");
164+
} else {
165+
globalThis.window = originalWindow;
166+
}
156167
});
157168

158169
it("injects the bridge script in serve mode", async () => {
@@ -200,4 +211,81 @@ describe("fmBridge", () => {
200211
await expect(plugin.transformIndexHtml?.("")).resolves.toBeUndefined();
201212
expect(globalThis.fetch).not.toHaveBeenCalled();
202213
});
214+
215+
it("warns and injects a fallback bridge when FM MCP responds with no connected files", async () => {
216+
vi.mocked(globalThis.fetch)
217+
.mockResolvedValueOnce(
218+
new Response(JSON.stringify([]), {
219+
status: 200,
220+
headers: { "Content-Type": "application/json" },
221+
}),
222+
)
223+
.mockResolvedValueOnce(
224+
new Response(JSON.stringify([]), {
225+
status: 200,
226+
headers: { "Content-Type": "application/json" },
227+
}),
228+
);
229+
230+
const plugin = fmBridge({
231+
fmMcpBaseUrl: "http://localhost:1365",
232+
});
233+
234+
if (typeof plugin.apply === "function") {
235+
expect(plugin.apply({} as never, { command: "serve", mode: "development" } as never)).toBe(true);
236+
}
237+
238+
await expect(plugin.configureServer?.({} as never)).resolves.toBeUndefined();
239+
expect(console.warn).toHaveBeenCalledWith(
240+
"fmBridge found no connected FileMaker files at http://localhost:1365/connectedFiles. Dev server will continue. Connect a FileMaker webviewer to enable bridge forwarding.",
241+
);
242+
243+
const tags = await plugin.transformIndexHtml?.("");
244+
245+
expect(tags).toHaveLength(1);
246+
expect(tags?.[0]).toMatchObject({
247+
tag: "script",
248+
injectTo: "head-prepend",
249+
});
250+
expect(tags?.[0]).toHaveProperty("children");
251+
});
252+
253+
it("logs runtime errors from the fallback bridge when no file is connected", async () => {
254+
vi.mocked(globalThis.fetch).mockResolvedValue(
255+
new Response(JSON.stringify([]), {
256+
status: 200,
257+
headers: { "Content-Type": "application/json" },
258+
}),
259+
);
260+
261+
const plugin = fmBridge({
262+
fmMcpBaseUrl: "http://localhost:1365",
263+
});
264+
265+
if (typeof plugin.apply === "function") {
266+
expect(plugin.apply({} as never, { command: "serve", mode: "development" } as never)).toBe(true);
267+
}
268+
269+
const tags = await plugin.transformIndexHtml?.("");
270+
const tag = tags?.[0];
271+
272+
expect(tag).toHaveProperty("children");
273+
274+
globalThis.window = {} as Window & typeof globalThis;
275+
new Function((tag as HtmlTagDescriptor & { children: string }).children)();
276+
277+
expect(typeof globalThis.window.filemaker).toBe("function");
278+
globalThis.window.filemaker?.("TestScript", "{}");
279+
globalThis.window.FileMaker?.PerformScript("TestScript", "{}");
280+
281+
expect(console.error).toHaveBeenCalledTimes(2);
282+
expect(console.error).toHaveBeenNthCalledWith(
283+
1,
284+
buildNoConnectedFilesRuntimeError("http://localhost:1365/connectedFiles"),
285+
);
286+
expect(console.error).toHaveBeenNthCalledWith(
287+
2,
288+
buildNoConnectedFilesRuntimeError("http://localhost:1365/connectedFiles"),
289+
);
290+
});
203291
});

0 commit comments

Comments
 (0)