Fix dev server on clean repo

This commit is contained in:
Konstantin Wohlwend 2026-05-06 13:51:15 -07:00
parent 765b0f4e29
commit bd8c4489ed
8 changed files with 128 additions and 7 deletions

View File

@ -391,3 +391,6 @@ A: The `/api/v1/internal/metrics` response now intentionally includes `analytics
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
A: The environment override endpoint validates the new environment override against the rendered branch config. Preview dummy payments data must therefore be internally coherent: products assigned to a product line need the same `customerType` as that product line, otherwise unrelated environment patches can fail with warnings like `Product "growth" has customer type "user" but its product line "workspace" has customer type "team"`.
## Q: Why can `pnpm run dev` fail with `ERR_MODULE_NOT_FOUND` for `@stackframe/stack/dist/esm/index.js` during OpenAPI docs generation?
A: Root `dev` starts the OpenAPI docs watcher at the same time as package `dev` watchers. If a package `dev` script removes `dist` before `tsdown --watch` recreates it, the docs generator can import `apps/backend/src/stack.tsx` while `@stackframe/stack`'s ESM entrypoint is temporarily missing. Package watch scripts should update `dist` in place, and eager generators should wait for package imports to resolve before running.

View File

@ -77,7 +77,7 @@
"generate-sdks": "pnpm exec tsx ./scripts/generate-sdks.ts",
"generate-setup-prompt-docs": "pnpm exec tsx ./scripts/generate-setup-prompt-docs.ts",
"generate-sdks:watch": "chokidar --silent -c 'pnpm run generate-sdks' './packages/template' --ignore './packages/template/package.json' --ignore '**/node_modules/**' --ignore '**/dist/**' --ignore '**/.turbo/**' --throttle 2000",
"generate-openapi-docs:watch": "pnpm run --filter=@stackframe/backend codegen-docs && chokidar --silent -c 'pnpm run --filter=@stackframe/backend codegen-docs' './apps/backend/src/app/api/latest/**/route.{js,jsx,ts,tsx}' './apps/backend/src/lib/openapi.ts' './packages/stack-shared/src/interface/webhooks.ts' --throttle 2000",
"generate-openapi-docs:watch": "pnpm exec tsx ./scripts/wait-for-dev-package-imports.ts && pnpm run --filter=@stackframe/backend codegen-docs && chokidar --silent -c 'pnpm run --filter=@stackframe/backend codegen-docs' './apps/backend/src/app/api/latest/**/route.{js,jsx,ts,tsx}' './apps/backend/src/lib/openapi.ts' './packages/stack-shared/src/interface/webhooks.ts' --throttle 2000",
"generate-setup-prompt-docs:watch": "pnpm run generate-setup-prompt-docs && chokidar --silent -c 'pnpm run generate-setup-prompt-docs' './packages/stack-shared/src/ai/prompts.ts' './scripts/generate-setup-prompt-docs.ts' --throttle 2000"
},
"devDependencies": {

View File

@ -41,7 +41,7 @@
"clean": "rimraf dist && rimraf node_modules",
"lint": "eslint --ext .tsx,.ts .",
"build": "rimraf dist && tsdown",
"dev": "rimraf dist && tsdown --watch"
"dev": "tsdown --watch"
},
"files": [
"README.md",

View File

@ -41,7 +41,7 @@
"clean": "rimraf dist && rimraf node_modules",
"lint": "eslint --ext .tsx,.ts .",
"build": "rimraf dist && pnpm run css && tsdown",
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"codegen": "pnpm run css",
"codegen:watch": "pnpm run css:watch",
"css": "pnpm run css-tw && pnpm run css-sc",

View File

@ -41,7 +41,7 @@
"clean": "rimraf dist && rimraf node_modules",
"lint": "eslint --ext .tsx,.ts .",
"build": "rimraf dist && pnpm run css && tsdown",
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"codegen": "pnpm run css",
"codegen:watch": "pnpm run css:watch",
"css": "pnpm run css-tw && pnpm run css-sc",

View File

@ -53,12 +53,12 @@
"//": "IF_PLATFORM template react-like",
"build": "rimraf dist && pnpm run css && tsdown",
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"codegen": "pnpm run css",
"codegen:watch": "pnpm run css:watch"
,"//": "ELSE_PLATFORM",
"build": "rimraf dist && tsdown",
"dev": "rimraf dist && tsdown --watch"
"dev": "tsdown --watch"
,"//": "END_PLATFORM",
"//": "IF_PLATFORM template react-like"

View File

@ -42,7 +42,7 @@
"clean": "rimraf dist && rimraf node_modules",
"lint": "eslint --ext .tsx,.ts .",
"build": "rimraf dist && pnpm run css && tsdown",
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"codegen": "pnpm run css",
"codegen:watch": "concurrently -n \"css\" -k \"pnpm run css:watch\"",
"css": "pnpm run css-tw && pnpm run css-sc",

View File

@ -0,0 +1,118 @@
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);
},
);