stack/scripts/wait-for-dev-package-imports.ts
BilalG1 609579abab
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
feat(hexclave): PR 3 — native @hexclave/* source rename + delete dual-publish wiring (#1482)
2026-05-29 15:21:59 -07:00

132 lines
4.5 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.
//
// In addition to workspace packages, the probe checks that the generated Prisma
// client exists. When `turbo run dev` starts the backend, `codegen-prisma:watch`
// (`prisma generate --watch`) performs an initial generation that briefly removes
// and recreates `src/generated/prisma/`. If `codegen-docs` runs during that
// window it fails with ERR_MODULE_NOT_FOUND for `@/generated/prisma/client`.
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('@hexclave/next');
await import('@hexclave/shared/dist/utils/env');
const { existsSync, readdirSync } = await import('node:fs');
const { join } = await import('node:path');
const generatedDir = join(process.cwd(), 'src', 'generated', 'prisma');
if (!existsSync(generatedDir) || readdirSync(generatedDir).length === 0) {
const err = new Error('ERR_MODULE_NOT_FOUND: Generated Prisma client not yet available at ' + generatedDir);
throw err;
}
})().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);
},
);