* - Updated `frontend/src/hooks/useDevserverUrl.ts:66-84` so new log chunks are scanned in arrival order and the state keeps the very first detected preview URL/port instead of replacing it with later ones. - Confirmed the hook still resets when the log stream shrinks (new process) so a new attempt can detect a fresh first port. I wasn’t able to run `pnpm run check` here; feel free to run it locally if you’d like to double-check. * - Added an early return in `frontend/src/hooks/useDevserverUrl.ts:74-81` so once a preview URL is locked in we simply advance `lastIndexRef` and skip parsing new log entries, avoiding unnecessary work while still resetting correctly if the stream shrinks. - Extended the effect dependency list to include `urlInfo` so the early-return logic behaves consistently with state updates. If you want to cover the edge case where the dev server restarts on the same attempt and we intentionally reset the stored URL, we can hook into that next. * Swapped the index-based `for` with `Array.prototype.some` over a sliced view of the new log entries (`frontend/src/hooks/useDevserverUrl.ts:77-85`). This keeps the “stop at the first hit” behavior but reads a little cleaner and avoids manual index bookkeeping.
96 lines
2.4 KiB
TypeScript
96 lines
2.4 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { stripAnsi } from 'fancy-ansi';
|
|
|
|
const urlPatterns = [
|
|
/(https?:\/\/(?:\[[0-9a-f:]+\]|localhost|127\.0\.0\.1|0\.0\.0\.0|\d{1,3}(?:\.\d{1,3}){3})(?::\d{2,5})?(?:\/\S*)?)/i,
|
|
/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[[0-9a-f:]+\]|(?:\d{1,3}\.){3}\d{1,3}):(\d{2,5})/i,
|
|
];
|
|
|
|
export type DevserverUrlInfo = {
|
|
url: string;
|
|
port?: number;
|
|
scheme: 'http' | 'https';
|
|
};
|
|
|
|
export const detectDevserverUrl = (line: string): DevserverUrlInfo | null => {
|
|
const cleaned = stripAnsi(line);
|
|
|
|
const fullUrlMatch = urlPatterns[0].exec(cleaned);
|
|
if (fullUrlMatch) {
|
|
try {
|
|
const parsed = new URL(fullUrlMatch[1]);
|
|
if (
|
|
parsed.hostname === '0.0.0.0' ||
|
|
parsed.hostname === '::' ||
|
|
parsed.hostname === '[::]'
|
|
) {
|
|
parsed.hostname = 'localhost';
|
|
}
|
|
return {
|
|
url: parsed.toString(),
|
|
port: parsed.port ? Number(parsed.port) : undefined,
|
|
scheme: parsed.protocol === 'https:' ? 'https' : 'http',
|
|
};
|
|
} catch {
|
|
// Ignore invalid URLs and fall through to host:port detection.
|
|
}
|
|
}
|
|
|
|
const hostPortMatch = urlPatterns[1].exec(cleaned);
|
|
if (hostPortMatch) {
|
|
const port = Number(hostPortMatch[1]);
|
|
const scheme = /https/i.test(cleaned) ? 'https' : 'http';
|
|
return {
|
|
url: `${scheme}://localhost:${port}`,
|
|
port,
|
|
scheme: scheme as 'http' | 'https',
|
|
};
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
export const useDevserverUrlFromLogs = (
|
|
logs: Array<{ content: string }> | undefined
|
|
): DevserverUrlInfo | undefined => {
|
|
const [urlInfo, setUrlInfo] = useState<DevserverUrlInfo | undefined>();
|
|
const lastIndexRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
if (!logs) {
|
|
setUrlInfo(undefined);
|
|
lastIndexRef.current = 0;
|
|
return;
|
|
}
|
|
|
|
if (logs.length < lastIndexRef.current) {
|
|
lastIndexRef.current = 0;
|
|
setUrlInfo(undefined);
|
|
}
|
|
|
|
if (urlInfo) {
|
|
lastIndexRef.current = logs.length;
|
|
return;
|
|
}
|
|
|
|
let detectedUrl: DevserverUrlInfo | undefined;
|
|
const newEntries = logs.slice(lastIndexRef.current);
|
|
newEntries.some((entry) => {
|
|
const detected = detectDevserverUrl(entry.content);
|
|
if (detected) {
|
|
detectedUrl = detected;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (detectedUrl) {
|
|
setUrlInfo((prev) => prev ?? detectedUrl);
|
|
}
|
|
|
|
lastIndexRef.current = logs.length;
|
|
}, [logs, urlInfo]);
|
|
|
|
return urlInfo;
|
|
};
|