mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🐛 fix: import button from ui root (#15599)
This commit is contained in:
+143
-77
@@ -1,34 +1,20 @@
|
||||
import { type ChildProcess, spawn } from 'node:child_process';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import dotenvExpand from 'dotenv-expand';
|
||||
import net from 'node:net';
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const shellEnv = Object.entries(process.env).reduce<Record<string, string>>(
|
||||
(acc, [key, value]) => {
|
||||
if (typeof value === 'string') acc[key] = value;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const dotenvEnv: Record<string, string> = {};
|
||||
const dotenvResult = dotenv.config({
|
||||
override: true,
|
||||
path: ['.env', `.env.${env}`, `.env.${env}.local`],
|
||||
processEnv: dotenvEnv,
|
||||
});
|
||||
|
||||
if (dotenvResult.parsed) {
|
||||
const expanded = dotenvExpand.expand({
|
||||
parsed: dotenvResult.parsed,
|
||||
processEnv: { ...dotenvEnv, ...shellEnv },
|
||||
});
|
||||
|
||||
Object.assign(process.env, expanded.parsed, shellEnv);
|
||||
interface DevProcessHandle {
|
||||
directPid?: number;
|
||||
groupPid?: number;
|
||||
isWindows: boolean;
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const NEXT_HOST = 'localhost';
|
||||
|
||||
/**
|
||||
@@ -44,16 +30,19 @@ const resolveNextPort = (): number => {
|
||||
return 3010;
|
||||
};
|
||||
|
||||
const NEXT_PORT = resolveNextPort();
|
||||
const NEXT_ROOT_URL = `http://${NEXT_HOST}:${NEXT_PORT}/`;
|
||||
const NEXT_READY_TIMEOUT_MS = 180_000;
|
||||
const NEXT_READY_RETRY_MS = 400;
|
||||
const FORCE_KILL_TIMEOUT_MS = 5_000;
|
||||
|
||||
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
|
||||
|
||||
let nextPort = 3010;
|
||||
let nextRootUrl = `http://${NEXT_HOST}:${nextPort}/`;
|
||||
let nextProcess: ChildProcess | undefined;
|
||||
let viteProcess: ChildProcess | undefined;
|
||||
let nextHandle: DevProcessHandle | undefined;
|
||||
let viteHandle: DevProcessHandle | undefined;
|
||||
let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let shuttingDown = false;
|
||||
|
||||
const runNpmScript = (scriptName: string) =>
|
||||
@@ -64,6 +53,66 @@ const runNpmScript = (scriptName: string) =>
|
||||
shell: isWindows,
|
||||
});
|
||||
|
||||
const loadEnv = () => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const shellEnv = Object.entries(process.env).reduce<Record<string, string>>(
|
||||
(acc, [key, value]) => {
|
||||
if (typeof value === 'string') acc[key] = value;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const dotenvEnv: Record<string, string> = {};
|
||||
const dotenvResult = dotenv.config({
|
||||
override: true,
|
||||
path: ['.env', `.env.${env}`, `.env.${env}.local`],
|
||||
processEnv: dotenvEnv,
|
||||
});
|
||||
|
||||
if (!dotenvResult.parsed) return;
|
||||
|
||||
const expanded = dotenvExpand.expand({
|
||||
parsed: dotenvResult.parsed,
|
||||
processEnv: { ...dotenvEnv, ...shellEnv },
|
||||
});
|
||||
|
||||
Object.assign(process.env, expanded.parsed, shellEnv);
|
||||
};
|
||||
|
||||
const createDevProcessHandle = ({
|
||||
isWindows,
|
||||
pid,
|
||||
}: {
|
||||
isWindows: boolean;
|
||||
pid?: number;
|
||||
}): DevProcessHandle => ({
|
||||
directPid: pid,
|
||||
groupPid: isWindows ? undefined : pid,
|
||||
isWindows,
|
||||
});
|
||||
|
||||
const sendSignalToDevProcess = (handle: DevProcessHandle | undefined, signal: NodeJS.Signals) => {
|
||||
if (!handle) return;
|
||||
|
||||
if (!handle.isWindows && handle.groupPid) {
|
||||
try {
|
||||
process.kill(-handle.groupPid, signal);
|
||||
return;
|
||||
} catch {
|
||||
// Fall through to the direct child pid below. The wrapper may already be
|
||||
// gone while its process group has been reaped.
|
||||
}
|
||||
}
|
||||
|
||||
if (!handle.directPid) return;
|
||||
|
||||
try {
|
||||
process.kill(handle.directPid, signal);
|
||||
} catch {
|
||||
// The process already exited.
|
||||
}
|
||||
};
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const isPortOpen = (host: string, port: number) =>
|
||||
@@ -84,25 +133,27 @@ const waitForNextReady = async () => {
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < NEXT_READY_TIMEOUT_MS) {
|
||||
if (await isPortOpen(NEXT_HOST, NEXT_PORT)) return;
|
||||
if (await isPortOpen(NEXT_HOST, nextPort)) return;
|
||||
await wait(NEXT_READY_RETRY_MS);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Next server was not ready within ${NEXT_READY_TIMEOUT_MS / 1000}s on ${NEXT_HOST}:${NEXT_PORT}`,
|
||||
`Next server was not ready within ${NEXT_READY_TIMEOUT_MS / 1000}s on ${NEXT_HOST}:${nextPort}`,
|
||||
);
|
||||
};
|
||||
|
||||
const prewarmNextRootCompile = async () => {
|
||||
const startedAt = Date.now();
|
||||
const response = await fetch(NEXT_ROOT_URL, { signal: AbortSignal.timeout(120_000) });
|
||||
const response = await fetch(nextRootUrl, { signal: AbortSignal.timeout(120_000) });
|
||||
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(2);
|
||||
console.log(`✅ Next prewarm request finished (${response.status}) in ${elapsed}s ${NEXT_ROOT_URL}`);
|
||||
console.log(
|
||||
`✅ Next prewarm request finished (${response.status}) in ${elapsed}s ${nextRootUrl}`,
|
||||
);
|
||||
};
|
||||
|
||||
const runNextBackgroundTasks = () => {
|
||||
setTimeout(() => {
|
||||
console.log(`🔁 Next server URL: ${NEXT_ROOT_URL}`);
|
||||
console.log(`🔁 Next server URL: ${nextRootUrl}`);
|
||||
}, 2_000);
|
||||
|
||||
void (async () => {
|
||||
@@ -115,67 +166,69 @@ const runNextBackgroundTasks = () => {
|
||||
})();
|
||||
};
|
||||
|
||||
const isChildAlive = (child: ChildProcess) =>
|
||||
!child.killed && child.exitCode === null && child.signalCode === null;
|
||||
|
||||
const sendKillSignal = (child: ChildProcess, signal: NodeJS.Signals) => {
|
||||
if (!isChildAlive(child) || !child.pid) return;
|
||||
try {
|
||||
if (!isWindows) {
|
||||
try {
|
||||
process.kill(-child.pid, signal);
|
||||
return;
|
||||
} catch {
|
||||
// process group kill failed; fall through to direct kill
|
||||
}
|
||||
}
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
// child already gone
|
||||
}
|
||||
const terminateChildren = () => {
|
||||
sendSignalToDevProcess(viteHandle, 'SIGTERM');
|
||||
sendSignalToDevProcess(nextHandle, 'SIGTERM');
|
||||
};
|
||||
|
||||
const terminateChild = (child?: ChildProcess) => {
|
||||
if (!child) return;
|
||||
sendKillSignal(child, 'SIGTERM');
|
||||
const forceKillChildren = () => {
|
||||
sendSignalToDevProcess(viteHandle, 'SIGKILL');
|
||||
sendSignalToDevProcess(nextHandle, 'SIGKILL');
|
||||
};
|
||||
|
||||
const forceKillChild = (child?: ChildProcess) => {
|
||||
if (!child) return;
|
||||
sendKillSignal(child, 'SIGKILL');
|
||||
const clearForceKillTimer = () => {
|
||||
if (!forceKillTimer) return;
|
||||
clearTimeout(forceKillTimer);
|
||||
forceKillTimer = undefined;
|
||||
};
|
||||
|
||||
const hasChildSettled = (child?: ChildProcess) =>
|
||||
!child || child.exitCode !== null || child.signalCode !== null;
|
||||
|
||||
const clearForceKillTimerWhenChildrenSettle = () => {
|
||||
if (!shuttingDown) return;
|
||||
if (hasChildSettled(nextProcess) && hasChildSettled(viteProcess)) clearForceKillTimer();
|
||||
};
|
||||
|
||||
const shutdownAll = (signal: NodeJS.Signals) => {
|
||||
if (shuttingDown) return;
|
||||
if (shuttingDown) {
|
||||
forceKillChildren();
|
||||
return;
|
||||
}
|
||||
shuttingDown = true;
|
||||
|
||||
terminateChild(viteProcess);
|
||||
terminateChild(nextProcess);
|
||||
terminateChildren();
|
||||
|
||||
process.exitCode = signal === 'SIGINT' ? 130 : 143;
|
||||
|
||||
const forceKillTimer = setTimeout(() => {
|
||||
forceKillChild(viteProcess);
|
||||
forceKillChild(nextProcess);
|
||||
forceKillTimer = setTimeout(() => {
|
||||
forceKillTimer = undefined;
|
||||
forceKillChildren();
|
||||
}, FORCE_KILL_TIMEOUT_MS);
|
||||
forceKillTimer.unref();
|
||||
};
|
||||
|
||||
const watchChildExit = (child: ChildProcess, name: 'next' | 'vite') => {
|
||||
child.once('exit', (code, signal) => {
|
||||
if (!shuttingDown) {
|
||||
console.error(
|
||||
`❌ ${name} exited unexpectedly (code: ${code ?? 'null'}, signal: ${signal ?? 'null'})`,
|
||||
);
|
||||
shutdownAll('SIGTERM');
|
||||
if (shuttingDown) {
|
||||
clearForceKillTimerWhenChildrenSettle();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`❌ ${name} exited unexpectedly (code: ${code ?? 'null'}, signal: ${signal ?? 'null'})`,
|
||||
);
|
||||
shutdownAll('SIGTERM');
|
||||
});
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
loadEnv();
|
||||
nextPort = resolveNextPort();
|
||||
nextRootUrl = `http://${NEXT_HOST}:${nextPort}/`;
|
||||
|
||||
const forwardedSignals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
||||
for (const sig of forwardedSignals) {
|
||||
process.once(sig, () => shutdownAll(sig));
|
||||
process.on(sig, () => shutdownAll(sig));
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
@@ -189,19 +242,20 @@ const main = async () => {
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
forceKillChild(viteProcess);
|
||||
forceKillChild(nextProcess);
|
||||
forceKillChildren();
|
||||
});
|
||||
|
||||
nextProcess = spawn('npx', ['next', 'dev', '-p', String(NEXT_PORT)], {
|
||||
nextProcess = spawn('npx', ['next', 'dev', '-p', String(nextPort)], {
|
||||
detached: !isWindows,
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
shell: isWindows,
|
||||
});
|
||||
nextHandle = createDevProcessHandle({ isWindows, pid: nextProcess.pid });
|
||||
watchChildExit(nextProcess, 'next');
|
||||
|
||||
viteProcess = runNpmScript('dev:spa');
|
||||
viteHandle = createDevProcessHandle({ isWindows, pid: viteProcess.pid });
|
||||
watchChildExit(viteProcess, 'vite');
|
||||
runNextBackgroundTasks();
|
||||
|
||||
@@ -211,7 +265,19 @@ const main = async () => {
|
||||
]);
|
||||
};
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error('❌ dev startup sequence failed:', error);
|
||||
shutdownAll('SIGTERM');
|
||||
});
|
||||
const isMainModule = () => {
|
||||
const entry = process.argv[1];
|
||||
return !!entry && import.meta.url === pathToFileURL(path.resolve(entry)).href;
|
||||
};
|
||||
|
||||
export const __testing = {
|
||||
createDevProcessHandle,
|
||||
sendSignalToDevProcess,
|
||||
};
|
||||
|
||||
if (isMainModule()) {
|
||||
void main().catch((error) => {
|
||||
console.error('❌ dev startup sequence failed:', error);
|
||||
shutdownAll('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
||||
import { Flexbox, Icon } from '@lobehub/ui';
|
||||
import { Button, Flexbox, Icon } from '@lobehub/ui';
|
||||
import {
|
||||
Button,
|
||||
createModal,
|
||||
type ImperativeModalProps,
|
||||
ModalFooter,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import type { TaskTemplate } from '@lobechat/const';
|
||||
import { ActionIcon, Flexbox, Icon, Markdown, Text } from '@lobehub/ui';
|
||||
import { Button, createModal, type ModalInstance, useModalContext } from '@lobehub/ui/base-ui';
|
||||
import { ActionIcon, Button, Flexbox, Icon, Markdown, Text } from '@lobehub/ui';
|
||||
import { createModal, type ModalInstance, useModalContext } from '@lobehub/ui/base-ui';
|
||||
import { Divider } from 'antd';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { Clock, X } from 'lucide-react';
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
interface DevProcessHandle {
|
||||
directPid?: number;
|
||||
groupPid?: number;
|
||||
isWindows: boolean;
|
||||
}
|
||||
|
||||
interface DevStartupTestingExports {
|
||||
__testing: {
|
||||
createDevProcessHandle: (params: { isWindows: boolean; pid?: number }) => DevProcessHandle;
|
||||
sendSignalToDevProcess: (handle: DevProcessHandle | undefined, signal: NodeJS.Signals) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const loadTestingExports = async () => {
|
||||
const modulePath = '../../scripts/devStartupSequence' + '.mts';
|
||||
return (await import(modulePath)) as unknown as DevStartupTestingExports;
|
||||
};
|
||||
|
||||
describe('devProcessCleanup', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should save the detached process group pid on Unix', async () => {
|
||||
const { createDevProcessHandle } = (await loadTestingExports()).__testing;
|
||||
|
||||
expect(createDevProcessHandle({ isWindows: false, pid: 1234 })).toEqual({
|
||||
directPid: 1234,
|
||||
groupPid: 1234,
|
||||
isWindows: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should signal the saved process group without requiring the direct child to be alive', async () => {
|
||||
const { sendSignalToDevProcess } = (await loadTestingExports()).__testing;
|
||||
const kill = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
||||
|
||||
sendSignalToDevProcess(
|
||||
{
|
||||
directPid: 1234,
|
||||
groupPid: 1234,
|
||||
isWindows: false,
|
||||
},
|
||||
'SIGTERM',
|
||||
);
|
||||
|
||||
expect(kill).toHaveBeenCalledTimes(1);
|
||||
expect(kill).toHaveBeenCalledWith(-1234, 'SIGTERM');
|
||||
});
|
||||
|
||||
it('should fall back to the direct child pid when the process group is already gone', async () => {
|
||||
const { sendSignalToDevProcess } = (await loadTestingExports()).__testing;
|
||||
const kill = vi.spyOn(process, 'kill').mockImplementation((pid) => {
|
||||
if (pid < 0) throw new Error('missing process group');
|
||||
return true;
|
||||
});
|
||||
|
||||
sendSignalToDevProcess(
|
||||
{
|
||||
directPid: 1234,
|
||||
groupPid: 1234,
|
||||
isWindows: false,
|
||||
},
|
||||
'SIGKILL',
|
||||
);
|
||||
|
||||
expect(kill).toHaveBeenCalledTimes(2);
|
||||
expect(kill).toHaveBeenNthCalledWith(1, -1234, 'SIGKILL');
|
||||
expect(kill).toHaveBeenNthCalledWith(2, 1234, 'SIGKILL');
|
||||
});
|
||||
|
||||
it('should signal only the direct child pid on Windows', async () => {
|
||||
const { createDevProcessHandle, sendSignalToDevProcess } = (await loadTestingExports())
|
||||
.__testing;
|
||||
const kill = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
||||
|
||||
sendSignalToDevProcess(createDevProcessHandle({ isWindows: true, pid: 1234 }), 'SIGTERM');
|
||||
|
||||
expect(kill).toHaveBeenCalledTimes(1);
|
||||
expect(kill).toHaveBeenCalledWith(1234, 'SIGTERM');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user