forked from molecule-ai/molecule-core
fix(canvas): load monorepo .env in next.config so WS connects in dev
Symptom: spawn animation missing on org import. Workspaces appeared in their final positions all at once instead of materialising one-by-one. Root cause: the WS pill said "Reconnecting" forever because the canvas was trying to connect to ws://localhost:3000/ws — its own port, where Next.js dev doesn't serve a WebSocket — instead of the platform's ws://localhost:8080/ws. Why: deriveWsBaseUrl() falls back to window.location when NEXT_PUBLIC_WS_URL is unset. Next.js auto-loads .env from the project root only — and the canonical NEXT_PUBLIC_WS_URL / NEXT_PUBLIC_PLATFORM_URL live in the monorepo root .env, alongside the Go platform's MOLECULE_ENV / DATABASE_URL. Without an extra canvas/.env.local copy (which would still be a per-developer manual step), the canvas dev server starts blind to those vars. Fix: next.config.ts now walks upward from __dirname looking for the monorepo root (same workspace-server/go.mod sentinel the platform's dotenv loader uses) and merges the root .env into process.env BEFORE Next.js compiles. Existing env wins over file values, so docker runs / CI / explicit exports still dominate. The parser is a TypeScript mirror of workspace-server/cmd/server/ dotenv.go's parseDotEnvLine — same rules (export prefix, quotes, inline comments, BOM) so a single .env line behaves identically across both processes. If one parser changes, the other has to. Production unaffected: `output: "standalone"` bakes resolved env into the build, the workspace-server sentinel isn't shipped in deploy artifacts, and the existing-env-wins rule means container env dominates anywhere this file is consulted at runtime. Verified: canvas dev startup log now shows "[next.config] loaded 49 vars from /Users/.../molecule-core/.env"; served bundle has the correct ws://localhost:8080/ws URL; WS pill flips to "Connected" after a hard refresh and per-workspace spawn animations fire on the next org import as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4014513b94
commit
ec7ecd5461
@ -1,7 +1,88 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Load NEXT_PUBLIC_* vars from the monorepo root .env so a fresh
|
||||
// `pnpm dev` works without a per-developer canvas/.env.local. Next.js
|
||||
// only auto-loads .env from the project root by default — but our
|
||||
// canonical config (NEXT_PUBLIC_PLATFORM_URL, NEXT_PUBLIC_WS_URL,
|
||||
// MOLECULE_ENV, etc.) lives at the monorepo root, gitignored, shared
|
||||
// by the Go platform binary. Without this, the canvas falls back to
|
||||
// `window.location` (`ws://localhost:3000/ws`) and the WS pill stays
|
||||
// "Reconnecting" forever because Next.js dev doesn't serve /ws.
|
||||
//
|
||||
// Mirrors workspace-server/cmd/server/dotenv.go's monorepo-rooted .env
|
||||
// loader. Both processes look for the SAME marker (`workspace-server/
|
||||
// go.mod`) so a developer renaming or relocating the repo only has to
|
||||
// update one heuristic. Production is unaffected: `output: "standalone"`
|
||||
// bakes resolved env into the build, and the marker file isn't shipped.
|
||||
loadMonorepoEnv();
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
function loadMonorepoEnv() {
|
||||
const root = findMonorepoRoot(__dirname);
|
||||
if (!root) return;
|
||||
const envPath = join(root, ".env");
|
||||
if (!existsSync(envPath)) return;
|
||||
const body = readFileSync(envPath, "utf8");
|
||||
let loaded = 0;
|
||||
let skipped = 0;
|
||||
for (const line of body.split(/\r?\n/)) {
|
||||
const kv = parseLine(line);
|
||||
if (!kv) continue;
|
||||
const [k, v] = kv;
|
||||
if (process.env[k] !== undefined) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
process.env[k] = v;
|
||||
loaded++;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[next.config] loaded ${loaded} vars from ${envPath} (${skipped} already set in env)`,
|
||||
);
|
||||
}
|
||||
|
||||
function findMonorepoRoot(start: string): string | null {
|
||||
let dir = start;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (existsSync(join(dir, "workspace-server", "go.mod"))) return dir;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mirror of workspace-server/cmd/server/dotenv.go's parseDotEnvLine
|
||||
// — same rules so the two loaders agree on every line in the shared
|
||||
// .env. If you change one parser, change the other.
|
||||
function parseLine(raw: string): [string, string] | null {
|
||||
let line = raw.replace(/^/, "").trim();
|
||||
if (line === "" || line.startsWith("#")) return null;
|
||||
if (line.startsWith("export ")) line = line.slice("export ".length).trimStart();
|
||||
const eq = line.indexOf("=");
|
||||
if (eq <= 0) return null;
|
||||
const k = line.slice(0, eq).trim();
|
||||
let v = line.slice(eq + 1).replace(/^[ \t]+/, "");
|
||||
if (v.length >= 2 && (v[0] === '"' || v[0] === "'")) {
|
||||
const quote = v[0];
|
||||
const end = v.indexOf(quote, 1);
|
||||
if (end >= 0) return [k, v.slice(1, end)];
|
||||
// unterminated — fall through to bare-value handling
|
||||
}
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
if (v[i] !== "#") continue;
|
||||
if (i === 0 || v[i - 1] === " " || v[i - 1] === "\t") {
|
||||
v = v.slice(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [k, v.trim()];
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user