mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
`pnpm dev:app <path>` appended `--config {devUrl}` after the path, and `tauri dev`
hands the first positional argument and everything after it to the app. The config was
dropped: tauri ran `pnpm dev` on port 1420 as well, and a full reload loaded that server
instead of the worktree's, failing with "Could not load startup script". The config now goes
before the first positional argument.
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import { realpathSync } from "node:fs";
|
|
import { createConnection, createServer, type Server } from "node:net";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export const worktreeRoot = realpathSync(fileURLToPath(new URL("..", import.meta.url)));
|
|
|
|
export function worktreeId(root: string): string {
|
|
let hash = 0xcbf29ce484222325n;
|
|
for (const byte of Buffer.from(root)) {
|
|
hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
|
|
}
|
|
return `ai.l0ng.openidea.dev.w${hash.toString(16).padStart(16, "0")}`;
|
|
}
|
|
|
|
export interface DevSession {
|
|
worktree: string;
|
|
identifier: string;
|
|
pid: number;
|
|
devUrl?: string;
|
|
}
|
|
|
|
export function preferredVitePort(identifier: string): number {
|
|
return 15000 + (Number.parseInt(identifier.slice(-8), 16) % 20000);
|
|
}
|
|
|
|
function readSession(port: number): Promise<DevSession | undefined> {
|
|
return new Promise((resolve) => {
|
|
const socket = createConnection({ host: "127.0.0.1", port });
|
|
let data = "";
|
|
const finish = (session?: DevSession) => {
|
|
socket.destroy();
|
|
resolve(session);
|
|
};
|
|
socket.setTimeout(1000, () => finish());
|
|
socket.on("error", () => finish());
|
|
socket.on("data", (chunk) => {
|
|
data += chunk.toString();
|
|
if (data.length > 16384) finish();
|
|
});
|
|
socket.on("end", () => {
|
|
try {
|
|
finish(JSON.parse(data));
|
|
} catch {
|
|
finish();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// A live OS listener is the lock: no stale PID files after crashes or PID reuse.
|
|
export async function claimSession(
|
|
session: DevSession,
|
|
firstPort = 43000 + (Number.parseInt(session.identifier.slice(-8), 16) % 10000),
|
|
): Promise<{ server: Server } | { existing: DevSession }> {
|
|
for (let offset = 0; offset < 100; offset++) {
|
|
const port = firstPort + offset;
|
|
const server = createServer((socket) => {
|
|
socket.on("error", () => {});
|
|
socket.end(JSON.stringify(session));
|
|
});
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(port, "127.0.0.1", () => {
|
|
server.removeListener("error", reject);
|
|
resolve();
|
|
});
|
|
});
|
|
return { server };
|
|
} catch (error) {
|
|
const code = (error as NodeJS.ErrnoException).code;
|
|
if (code !== "EADDRINUSE" && code !== "EACCES") throw error;
|
|
const existing = await readSession(port);
|
|
if (existing?.worktree === session.worktree && existing.identifier === session.identifier) {
|
|
return { existing };
|
|
}
|
|
}
|
|
}
|
|
throw new Error("No available development session port");
|
|
}
|
|
|
|
/** `tauri dev` options that take a value as the next argument. */
|
|
const VALUE_OPTIONS = new Set(["-c", "--config", "-t", "--target", "-f", "--features", "-r", "--runner", "--port"]);
|
|
|
|
/**
|
|
* `args` with the session's Vite URL as the last `--config`, placed before the first
|
|
* positional argument: `tauri dev` passes everything from there on to the app, so a config
|
|
* after `pnpm dev:app <project path>` would be ignored.
|
|
*/
|
|
export function withDevConfig(args: string[], devUrl: string): string[] {
|
|
let index = 1;
|
|
while (index < args.length && args[index] !== "--" && args[index]!.startsWith("-")) {
|
|
index += VALUE_OPTIONS.has(args[index]!) ? 2 : 1;
|
|
}
|
|
index = Math.min(index, args.length);
|
|
return [
|
|
...args.slice(0, index),
|
|
"--config",
|
|
JSON.stringify({ build: { beforeDevCommand: "", devUrl } }),
|
|
...args.slice(index),
|
|
];
|
|
}
|