mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Fix dev server on clean repo
This commit is contained in:
parent
765b0f4e29
commit
bd8c4489ed
@ -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.
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
118
scripts/wait-for-dev-package-imports.ts
Normal file
118
scripts/wait-for-dev-package-imports.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue
Block a user