Skip to content

Commit 9fd788b

Browse files
authored
fix(preview): only show browser-ready local servers (#6021)
1 parent 710fd0e commit 9fd788b

15 files changed

Lines changed: 1065 additions & 213 deletions

apps/server/src/preview/PortScanner.test.ts

Lines changed: 532 additions & 25 deletions
Large diffs are not rendered by default.

apps/server/src/preview/PortScanner.ts

Lines changed: 305 additions & 31 deletions
Large diffs are not rendered by default.

apps/server/src/ws.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,23 +2131,31 @@ const makeWsRpcLayer = (
21312131
observeRpcStream(WS_METHODS.subscribePreviewEvents, previewManager.events, {
21322132
"rpc.aggregate": "preview",
21332133
}),
2134-
[WS_METHODS.subscribeDiscoveredLocalServers]: (_input) =>
2134+
[WS_METHODS.subscribeDiscoveredLocalServers]: (input) =>
21352135
observeRpcStream(
21362136
WS_METHODS.subscribeDiscoveredLocalServers,
21372137
Stream.callback<DiscoveredLocalServerList>((queue) =>
21382138
Effect.gen(function* () {
2139+
const configuredUrls = input.configuredUrls ?? [];
21392140
yield* portDiscovery.retain;
2140-
const initial = yield* portDiscovery.scan();
2141+
const initial = yield* portDiscovery.scan(configuredUrls);
21412142
const initialScannedAt = DateTime.formatIso(yield* DateTime.now);
21422143
yield* Queue.offer(queue, {
21432144
servers: initial,
21442145
scannedAt: initialScannedAt,
2146+
configuredUrlProbing: true,
21452147
});
2146-
yield* portDiscovery.subscribe((servers) =>
2147-
Effect.gen(function* () {
2148-
const scannedAt = DateTime.formatIso(yield* DateTime.now);
2149-
yield* Queue.offer(queue, { servers, scannedAt });
2150-
}),
2148+
yield* portDiscovery.subscribe(
2149+
{ configuredUrls, initialSnapshot: initial },
2150+
(servers) =>
2151+
Effect.gen(function* () {
2152+
const scannedAt = DateTime.formatIso(yield* DateTime.now);
2153+
yield* Queue.offer(queue, {
2154+
servers,
2155+
scannedAt,
2156+
configuredUrlProbing: true,
2157+
});
2158+
}),
21512159
);
21522160
}),
21532161
),

apps/web/src/components/preview/PreviewEmptyState.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const mocks = vi.hoisted(() => ({
1212
pid: number | null;
1313
terminal: null;
1414
source: "scanner";
15-
listening: boolean;
1615
}>,
1716
}));
1817

@@ -38,7 +37,6 @@ function server(port: number) {
3837
pid: 1,
3938
terminal: null,
4039
source: "scanner" as const,
41-
listening: true,
4240
};
4341
}
4442

apps/web/src/components/preview/PreviewEmptyState.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ interface Props {
1212
threadRef: ScopedThreadRef;
1313
environmentId: EnvironmentId;
1414
configuredUrls?: ReadonlyArray<string> | undefined;
15-
recentlySeenUrls?: ReadonlyArray<string> | undefined;
1615
recentEntries: ReadonlyArray<BrowserHistoryEntry>;
1716
onRemoveRecent: (url: string) => void;
1817
onOpenUrl: (url: string) => void;
@@ -22,15 +21,13 @@ export function PreviewEmptyState({
2221
threadRef,
2322
environmentId,
2423
configuredUrls,
25-
recentlySeenUrls,
2624
recentEntries,
2725
onRemoveRecent,
2826
onOpenUrl,
2927
}: Props) {
3028
const servers = useDiscoveredLocalServers({
3129
environmentId,
3230
configuredUrls,
33-
recentlySeenUrls,
3431
});
3532
const recents = recentEntries.filter((entry) => URL.canParse(entry.url)).slice(0, 8);
3633

@@ -42,7 +39,7 @@ export function PreviewEmptyState({
4239
</EmptyMedia>
4340
<EmptyTitle>No preview yet</EmptyTitle>
4441
<EmptyDescription>
45-
Type a URL above, or run a dev script. Listening localhost ports will show up here
42+
Type a URL above, or run a dev script. Browser-ready localhost servers will show up here
4643
automatically.
4744
</EmptyDescription>
4845
</Empty>
@@ -51,7 +48,7 @@ export function PreviewEmptyState({
5148

5249
return (
5350
<div className="flex h-full min-h-0 overflow-y-auto px-5 py-8">
54-
<div className="m-auto flex w-full max-w-xl flex-col gap-6">
51+
<div className="mx-auto flex w-full max-w-xl flex-col gap-6">
5552
{recents.length > 0 ? (
5653
<div className="flex flex-col gap-3">
5754
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -88,7 +85,7 @@ export function PreviewEmptyState({
8885
))}
8986
</div>
9087
<p className="px-1 text-xs text-muted-foreground">
91-
Select a listening port to open it in this browser tab.
88+
Select a live local server to open it in this browser tab.
9289
</p>
9390
</div>
9491
) : null}

apps/web/src/components/preview/PreviewLocalServerCard.tsx

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,11 @@ export function PreviewLocalServerCard({ threadRef, server, onOpen }: Props) {
2424
{server.host}:{server.port}
2525
</span>
2626
</div>
27-
{server.listening ? <PulsingDot /> : <DimDot />}
2827
</button>
2928
);
3029
}
3130

3231
function describeServer(server: PreviewableServer): string {
3332
if (server.processName) return server.processName;
34-
if (server.listening) return "Listening";
35-
if (server.source === "configured") return "Configured";
36-
return "Recently seen";
37-
}
38-
39-
function PulsingDot() {
40-
return (
41-
<span aria-label="Listening" className="relative inline-flex size-2 shrink-0">
42-
<span className="absolute inset-0 animate-status-ping rounded-full bg-success opacity-60" />
43-
<span className="relative inline-flex size-2 rounded-full bg-success" />
44-
</span>
45-
);
46-
}
47-
48-
function DimDot() {
49-
return (
50-
<span
51-
aria-label="Not currently listening"
52-
className="size-2 shrink-0 rounded-full bg-muted-foreground/40"
53-
/>
54-
);
33+
return "Listening";
5534
}

apps/web/src/components/preview/PreviewView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,6 @@ export function PreviewView({
713713
threadRef={threadRef}
714714
environmentId={threadRef.environmentId}
715715
configuredUrls={configuredUrls}
716-
recentlySeenUrls={previewState.recentlySeenUrls}
717716
recentEntries={recentHistoryEntries}
718717
onRemoveRecent={(url) => removeUrlForThread(threadRef, url)}
719718
onOpenUrl={(next) => void handleOpenServerUrl(next)}
Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DiscoveredLocalServer } from "@t3tools/contracts";
22
import { describe, expect, it } from "vite-plus/test";
33

4-
import { mergeServers, type PreviewableServer } from "./useDiscoveredLocalServers";
4+
import { mergeServers } from "./useDiscoveredLocalServers";
55

66
const scannerServer = (
77
overrides: Partial<DiscoveredLocalServer & { requestedUrl: string }>,
@@ -21,15 +21,13 @@ describe("mergeServers", () => {
2121
const result = mergeServers({
2222
scanner: [scannerServer({})],
2323
configuredUrls: [],
24-
recentlySeenUrls: [],
2524
});
2625
expect(result).toHaveLength(1);
2726
expect(result[0]).toMatchObject({
2827
host: "localhost",
2928
port: 5173,
3029
requestedUrl: "http://localhost:5173",
3130
source: "scanner",
32-
listening: true,
3331
processName: "vite",
3432
});
3533
});
@@ -38,102 +36,111 @@ describe("mergeServers", () => {
3836
const result = mergeServers({
3937
scanner: [scannerServer({ port: 5173, processName: "node", pid: 9999 })],
4038
configuredUrls: ["http://localhost:5173"],
41-
recentlySeenUrls: [],
4239
});
4340
expect(result).toHaveLength(1);
4441
expect(result[0]).toMatchObject({
4542
port: 5173,
4643
source: "configured",
47-
listening: true,
4844
processName: "node",
4945
pid: 9999,
5046
});
5147
});
5248

53-
it("keeps configured entries that the scanner doesn't see, with listening=false", () => {
49+
it("excludes configured entries that the live scanner doesn't see", () => {
5450
const result = mergeServers({
5551
scanner: [],
5652
configuredUrls: ["http://localhost:5173"],
57-
recentlySeenUrls: [],
5853
});
59-
expect(result).toHaveLength(1);
60-
expect(result[0]).toMatchObject({
61-
source: "configured",
62-
listening: false,
63-
requestedUrl: "http://localhost:5173/",
64-
});
65-
});
66-
67-
it("dedupes recently-seen URLs against scanner+configured entries", () => {
68-
const result = mergeServers({
69-
scanner: [scannerServer({ port: 5173 })],
70-
configuredUrls: [],
71-
recentlySeenUrls: ["http://localhost:5173/", "http://localhost:8080/"],
72-
});
73-
expect(result.map((s) => s.port)).toEqual([5173, 8080]);
74-
expect(result.find((s) => s.port === 5173)?.source).toBe("scanner");
75-
expect(result.find((s) => s.port === 8080)?.source).toBe("recent");
76-
expect(result.find((s) => s.port === 8080)?.requestedUrl).toBe("http://localhost:8080/");
54+
expect(result).toHaveLength(0);
7755
});
7856

79-
it("ignores non-loopback URLs in configured/recent inputs", () => {
57+
it("ignores non-loopback configured URLs", () => {
8058
const result = mergeServers({
81-
scanner: [],
59+
scanner: [scannerServer({})],
8260
configuredUrls: ["https://example.com", "ws://localhost:5173"],
83-
recentlySeenUrls: ["https://api.example.com"],
8461
});
85-
expect(result).toHaveLength(0);
62+
expect(result).toHaveLength(1);
63+
expect(result[0]?.source).toBe("scanner");
8664
});
8765

88-
it("sorts: configured before scanner before recent, then by port", () => {
66+
it("sorts configured live servers before scanner-only servers", () => {
8967
const result = mergeServers({
9068
scanner: [scannerServer({ port: 8080 }), scannerServer({ port: 3000 })],
91-
configuredUrls: ["http://localhost:5173"],
92-
recentlySeenUrls: ["http://localhost:9000/", "http://localhost:4321/"],
69+
configuredUrls: ["http://localhost:8080"],
9370
});
94-
expect(result.map((s) => `${s.source}:${s.port}`)).toEqual([
95-
"configured:5173",
96-
"scanner:3000",
97-
"scanner:8080",
98-
"recent:4321",
99-
"recent:9000",
100-
]);
71+
expect(result.map((s) => `${s.source}:${s.port}`)).toEqual(["configured:8080", "scanner:3000"]);
10172
});
10273

10374
it("dedupes by lowercased host", () => {
10475
const result = mergeServers({
10576
scanner: [scannerServer({ host: "Localhost", port: 5173 })],
10677
configuredUrls: ["http://localhost:5173"],
107-
recentlySeenUrls: [],
10878
});
10979
expect(result).toHaveLength(1);
80+
expect(result[0]?.source).toBe("configured");
11081
});
11182

112-
it("keeps a scanner entry's pre-resolution requestedUrl distinct from a resolved url", () => {
83+
it.each(["127.0.0.1", "0.0.0.0", "[::1]"])(
84+
"matches configured loopback alias %s to a live localhost server",
85+
(host) => {
86+
const result = mergeServers({
87+
scanner: [
88+
scannerServer({ requestedUrl: `http://localhost:5173/dashboard?mode=test#results` }),
89+
],
90+
configuredUrls: [`http://${host}:5173/dashboard?mode=test#results`],
91+
});
92+
expect(result).toHaveLength(1);
93+
expect(result[0]?.source).toBe("configured");
94+
expect(result[0]?.requestedUrl).toBe("http://localhost:5173/dashboard?mode=test#results");
95+
},
96+
);
97+
98+
it("keeps the scanner-verified path and protocol", () => {
11399
const result = mergeServers({
114100
scanner: [
115101
scannerServer({
116-
port: 5173,
117102
url: "https://env-42.example.dev:5173/",
118-
requestedUrl: "http://localhost:5173/",
103+
requestedUrl: "http://localhost:5173/dashboard?mode=test#results",
119104
}),
120105
],
121-
configuredUrls: [],
122-
recentlySeenUrls: [],
106+
configuredUrls: ["https://localhost:5173/dashboard?mode=test#results"],
123107
});
124108
expect(result[0]?.url).toBe("https://env-42.example.dev:5173/");
109+
expect(result[0]?.requestedUrl).toBe("http://localhost:5173/dashboard?mode=test#results");
110+
});
111+
112+
it("overlays a configured path when an older server does not advertise path probing", () => {
113+
const result = mergeServers({
114+
scanner: [scannerServer({ requestedUrl: "http://localhost:5173/" })],
115+
configuredUrls: ["https://localhost:5173/docs?mode=test#results"],
116+
configuredUrlProbing: false,
117+
});
118+
119+
expect(result[0]?.requestedUrl).toBe("https://localhost:5173/docs?mode=test#results");
120+
});
121+
122+
it("does not overlay an unverified configured path when the server probes paths", () => {
123+
const result = mergeServers({
124+
scanner: [scannerServer({ requestedUrl: "http://localhost:5173/" })],
125+
configuredUrls: ["http://localhost:5173/docs"],
126+
configuredUrlProbing: true,
127+
});
128+
125129
expect(result[0]?.requestedUrl).toBe("http://localhost:5173/");
126130
});
127-
});
128131

129-
describe("PreviewableServer interface", () => {
130-
it("preserves listening flag through enrichment", () => {
132+
it("keeps a scanner entry's pre-resolution requestedUrl distinct from a resolved url", () => {
131133
const result = mergeServers({
132-
scanner: [scannerServer({})],
133-
configuredUrls: ["http://localhost:5173"],
134-
recentlySeenUrls: [],
134+
scanner: [
135+
scannerServer({
136+
port: 5173,
137+
url: "https://env-42.example.dev:5173/",
138+
requestedUrl: "http://localhost:5173/",
139+
}),
140+
],
141+
configuredUrls: [],
135142
});
136-
const merged: PreviewableServer | undefined = result[0];
137-
expect(merged?.listening).toBe(true);
143+
expect(result[0]?.url).toBe("https://env-42.example.dev:5173/");
144+
expect(result[0]?.requestedUrl).toBe("http://localhost:5173/");
138145
});
139146
});

0 commit comments

Comments
 (0)