Skip to content

Commit bd8c448

Browse files
committed
Fix dev server on clean repo
1 parent 765b0f4 commit bd8c448

8 files changed

Lines changed: 128 additions & 7 deletions

File tree

.claude/CLAUDE-KNOWLEDGE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,6 @@ A: The `/api/v1/internal/metrics` response now intentionally includes `analytics
391391

392392
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
393393
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"`.
394+
395+
## Q: Why can `pnpm run dev` fail with `ERR_MODULE_NOT_FOUND` for `@stackframe/stack/dist/esm/index.js` during OpenAPI docs generation?
396+
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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"generate-sdks": "pnpm exec tsx ./scripts/generate-sdks.ts",
7878
"generate-setup-prompt-docs": "pnpm exec tsx ./scripts/generate-setup-prompt-docs.ts",
7979
"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",
80-
"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",
80+
"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",
8181
"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"
8282
},
8383
"devDependencies": {

packages/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"clean": "rimraf dist && rimraf node_modules",
4242
"lint": "eslint --ext .tsx,.ts .",
4343
"build": "rimraf dist && tsdown",
44-
"dev": "rimraf dist && tsdown --watch"
44+
"dev": "tsdown --watch"
4545
},
4646
"files": [
4747
"README.md",

packages/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"clean": "rimraf dist && rimraf node_modules",
4242
"lint": "eslint --ext .tsx,.ts .",
4343
"build": "rimraf dist && pnpm run css && tsdown",
44-
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
44+
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
4545
"codegen": "pnpm run css",
4646
"codegen:watch": "pnpm run css:watch",
4747
"css": "pnpm run css-tw && pnpm run css-sc",

packages/stack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"clean": "rimraf dist && rimraf node_modules",
4242
"lint": "eslint --ext .tsx,.ts .",
4343
"build": "rimraf dist && pnpm run css && tsdown",
44-
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
44+
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
4545
"codegen": "pnpm run css",
4646
"codegen:watch": "pnpm run css:watch",
4747
"css": "pnpm run css-tw && pnpm run css-sc",

packages/template/package-template.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@
5353

5454
"//": "IF_PLATFORM template react-like",
5555
"build": "rimraf dist && pnpm run css && tsdown",
56-
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
56+
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
5757
"codegen": "pnpm run css",
5858
"codegen:watch": "pnpm run css:watch"
5959
,"//": "ELSE_PLATFORM",
6060
"build": "rimraf dist && tsdown",
61-
"dev": "rimraf dist && tsdown --watch"
61+
"dev": "tsdown --watch"
6262
,"//": "END_PLATFORM",
6363

6464
"//": "IF_PLATFORM template react-like"

packages/template/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"clean": "rimraf dist && rimraf node_modules",
4343
"lint": "eslint --ext .tsx,.ts .",
4444
"build": "rimraf dist && pnpm run css && tsdown",
45-
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
45+
"dev": "concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
4646
"codegen": "pnpm run css",
4747
"codegen:watch": "concurrently -n \"css\" -k \"pnpm run css:watch\"",
4848
"css": "pnpm run css-tw && pnpm run css-sc",
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { spawn } from "child_process";
2+
import path from "path";
3+
import { setTimeout as sleep } from "timers/promises";
4+
5+
// Root `pnpm run dev` starts eager generators and package watch builds in
6+
// parallel. `generate-openapi-docs:watch` intentionally runs `codegen-docs`
7+
// once before starting chokidar, because chokidar only responds to future file
8+
// changes. Without that initial run, dev docs could serve stale OpenAPI JSON
9+
// from a previous branch, or no generated JSON at all after a clean checkout,
10+
// until someone edits an API route.
11+
//
12+
// That eager OpenAPI generation imports backend modules, and some of those
13+
// backend modules resolve workspace packages through their built `dist`
14+
// entrypoints. Package watch scripts update those entrypoints with
15+
// `tsdown --watch`, but on a cold checkout, after `pnpm clean`, or during the
16+
// first package watcher build, the entrypoints may not exist yet even though
17+
// `tsdown --watch` is about to create them.
18+
//
19+
// We keep this wait scoped to the eager generator rather than putting it in
20+
// front of backend `dev`: the long-running Next dev server can tolerate package
21+
// watchers warming up, while a one-shot generator exits immediately on a missing
22+
// import and `concurrently -k` then tears down the whole dev command. Package
23+
// watch scripts also avoid deleting `dist` in dev mode, which removes the
24+
// common restart race; this probe covers the remaining cold-start case.
25+
//
26+
// This probe waits only for the package imports that the backend-side generator
27+
// needs. It does not hide real runtime errors: we retry missing-module failures
28+
// while package builds warm up, and fail immediately for other import failures.
29+
const repoRoot = path.resolve(__dirname, "..");
30+
const backendDir = path.join(repoRoot, "apps/backend");
31+
const timeoutMs = 60_000;
32+
const retryDelayMs = 1_000;
33+
34+
const probeScript = `
35+
(async () => {
36+
await import('@stackframe/stack');
37+
await import('@stackframe/stack-shared/dist/utils/env');
38+
})().then(
39+
() => undefined,
40+
(error) => {
41+
console.error(error);
42+
process.exit(1);
43+
},
44+
);
45+
`;
46+
47+
type ProbeResult = {
48+
exitCode: number | null,
49+
output: string,
50+
};
51+
52+
function runProbe(): Promise<ProbeResult> {
53+
return new Promise((resolve, reject) => {
54+
const child = spawn("pnpm", ["exec", "tsx", "-e", probeScript], {
55+
cwd: backendDir,
56+
env: process.env,
57+
stdio: ["ignore", "pipe", "pipe"],
58+
});
59+
60+
let output = "";
61+
62+
child.stdout.setEncoding("utf8");
63+
child.stderr.setEncoding("utf8");
64+
child.stdout.on("data", (chunk: string) => {
65+
output += chunk;
66+
});
67+
child.stderr.on("data", (chunk: string) => {
68+
output += chunk;
69+
});
70+
71+
child.on("error", reject);
72+
child.on("close", (exitCode) => {
73+
resolve({ exitCode, output });
74+
});
75+
});
76+
}
77+
78+
function isMissingModuleError(output: string) {
79+
return output.includes("ERR_MODULE_NOT_FOUND") || output.includes("MODULE_NOT_FOUND");
80+
}
81+
82+
async function main() {
83+
const start = performance.now();
84+
let lastOutput = "";
85+
let hasLoggedWait = false;
86+
let isReady = false;
87+
88+
while (performance.now() - start < timeoutMs) {
89+
const result = await runProbe();
90+
if (result.exitCode === 0) {
91+
isReady = true;
92+
break;
93+
}
94+
95+
lastOutput = result.output;
96+
if (!isMissingModuleError(result.output)) {
97+
throw new Error(`Dev package import probe failed with a non-retryable error:\n${result.output}`);
98+
}
99+
100+
if (!hasLoggedWait) {
101+
console.log("Waiting for dev package entrypoints to be generated...");
102+
hasLoggedWait = true;
103+
}
104+
await sleep(retryDelayMs);
105+
}
106+
107+
if (!isReady) {
108+
throw new Error(`Timed out waiting for dev package imports to become available. Last probe output:\n${lastOutput}`);
109+
}
110+
}
111+
112+
main().then(
113+
() => undefined,
114+
(error) => {
115+
console.error(error);
116+
process.exit(1);
117+
},
118+
);

0 commit comments

Comments
 (0)