mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { spawn } from "child_process";
|
|
import path from "path";
|
|
import { setTimeout as sleep } from "timers/promises";
|
|
|
|
// Root `pnpm run dev` starts eager generators and package watch builds in
|
|
// parallel. `generate-openapi-docs:watch` intentionally runs `codegen-docs`
|
|
// once before starting chokidar, because chokidar only responds to future file
|
|
// changes. Without that initial run, dev docs could serve stale OpenAPI JSON
|
|
// from a previous branch, or no generated JSON at all after a clean checkout,
|
|
// until someone edits an API route.
|
|
//
|
|
// That eager OpenAPI generation imports backend modules, and some of those
|
|
// backend modules resolve workspace packages through their built `dist`
|
|
// entrypoints. Package watch scripts update those entrypoints with
|
|
// `tsdown --watch`, but on a cold checkout, after `pnpm clean`, or during the
|
|
// first package watcher build, the entrypoints may not exist yet even though
|
|
// `tsdown --watch` is about to create them.
|
|
//
|
|
// We keep this wait scoped to the eager generator rather than putting it in
|
|
// front of backend `dev`: the long-running Next dev server can tolerate package
|
|
// watchers warming up, while a one-shot generator exits immediately on a missing
|
|
// import and `concurrently -k` then tears down the whole dev command. Package
|
|
// watch scripts also avoid deleting `dist` in dev mode, which removes the
|
|
// common restart race; this probe covers the remaining cold-start case.
|
|
//
|
|
// This probe waits only for the package imports that the backend-side generator
|
|
// needs. It does not hide real runtime errors: we retry missing-module failures
|
|
// while package builds warm up, and fail immediately for other import failures.
|
|
const repoRoot = path.resolve(__dirname, "..");
|
|
const backendDir = path.join(repoRoot, "apps/backend");
|
|
const timeoutMs = 60_000;
|
|
const retryDelayMs = 1_000;
|
|
|
|
const probeScript = `
|
|
(async () => {
|
|
await import('@stackframe/stack');
|
|
await import('@stackframe/stack-shared/dist/utils/env');
|
|
})().then(
|
|
() => undefined,
|
|
(error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
},
|
|
);
|
|
`;
|
|
|
|
type ProbeResult = {
|
|
exitCode: number | null,
|
|
output: string,
|
|
};
|
|
|
|
function runProbe(): Promise<ProbeResult> {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn("pnpm", ["exec", "tsx", "-e", probeScript], {
|
|
cwd: backendDir,
|
|
env: process.env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
let output = "";
|
|
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk: string) => {
|
|
output += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk: string) => {
|
|
output += chunk;
|
|
});
|
|
|
|
child.on("error", reject);
|
|
child.on("close", (exitCode) => {
|
|
resolve({ exitCode, output });
|
|
});
|
|
});
|
|
}
|
|
|
|
function isMissingModuleError(output: string) {
|
|
return output.includes("ERR_MODULE_NOT_FOUND") || output.includes("MODULE_NOT_FOUND");
|
|
}
|
|
|
|
async function main() {
|
|
const start = performance.now();
|
|
let lastOutput = "";
|
|
let hasLoggedWait = false;
|
|
let isReady = false;
|
|
|
|
while (performance.now() - start < timeoutMs) {
|
|
const result = await runProbe();
|
|
if (result.exitCode === 0) {
|
|
isReady = true;
|
|
break;
|
|
}
|
|
|
|
lastOutput = result.output;
|
|
if (!isMissingModuleError(result.output)) {
|
|
throw new Error(`Dev package import probe failed with a non-retryable error:\n${result.output}`);
|
|
}
|
|
|
|
if (!hasLoggedWait) {
|
|
console.log("Waiting for dev package entrypoints to be generated...");
|
|
hasLoggedWait = true;
|
|
}
|
|
await sleep(retryDelayMs);
|
|
}
|
|
|
|
if (!isReady) {
|
|
throw new Error(`Timed out waiting for dev package imports to become available. Last probe output:\n${lastOutput}`);
|
|
}
|
|
}
|
|
|
|
main().then(
|
|
() => undefined,
|
|
(error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
},
|
|
);
|