1
This commit is contained in:
@@ -1,14 +1,26 @@
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {existsSync, readFileSync} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../', import.meta.url));
|
||||
const serverRoot = fileURLToPath(new URL('../server-node/', import.meta.url));
|
||||
const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url));
|
||||
const serverTsxCliPath = fileURLToPath(
|
||||
new URL('../server-node/node_modules/tsx/dist/cli.mjs', import.meta.url),
|
||||
);
|
||||
const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url));
|
||||
const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url));
|
||||
const bundledNodePath = fileURLToPath(
|
||||
new URL('../.tools/node-v22.22.2-win-x64/node.exe', import.meta.url),
|
||||
);
|
||||
const bundledNpmCliPath = fileURLToPath(
|
||||
new URL('../.tools/node-v22.22.2-win-x64/node_modules/npm/bin/npm-cli.js', import.meta.url),
|
||||
);
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const DEFAULT_DEV_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:5432/genarrative';
|
||||
const DEV_MEMORY_DATABASE_URL = 'pg-mem://genarrative-dev';
|
||||
|
||||
function parseEnvContents(contents) {
|
||||
return contents
|
||||
@@ -47,6 +59,44 @@ function readEnvFile(filePath) {
|
||||
return parseEnvContents(readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
function resolveDatabaseProbeTarget(databaseUrl) {
|
||||
const trimmed = databaseUrl.trim();
|
||||
if (!trimmed || !/^postgres(?:ql)?:\/\//u.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
return {
|
||||
host: url.hostname === '0.0.0.0' ? '127.0.0.1' : url.hostname,
|
||||
port: Number(url.port || 5432),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function checkTcpReachable(target, timeoutMs = 1500) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.createConnection(target);
|
||||
let settled = false;
|
||||
|
||||
const finish = (result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
socket.destroy();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
socket.setTimeout(timeoutMs);
|
||||
socket.once('connect', () => finish(true));
|
||||
socket.once('timeout', () => finish(false));
|
||||
socket.once('error', () => finish(false));
|
||||
});
|
||||
}
|
||||
|
||||
function resolveServerTarget(serverAddr) {
|
||||
const trimmed = serverAddr.trim();
|
||||
|
||||
@@ -77,23 +127,104 @@ function resolveServerTarget(serverAddr) {
|
||||
return `http://${trimmed}`;
|
||||
}
|
||||
|
||||
function redactDatabaseUrl(databaseUrl) {
|
||||
const trimmed = `${databaseUrl || ''}`.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return '[missing]';
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('pg-mem://')) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
const databaseName = url.pathname.replace(/^\/+/u, '') || 'postgres';
|
||||
const portSuffix = url.port ? `:${url.port}` : '';
|
||||
return `${url.protocol}//${url.hostname}${portSuffix}/${databaseName}`;
|
||||
} catch {
|
||||
return '[configured]';
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePathEnvKey(envMap) {
|
||||
return Object.keys(envMap).find((key) => key.toLowerCase() === 'path') || 'PATH';
|
||||
}
|
||||
|
||||
function prependEnvPath(envMap, nextEntry) {
|
||||
const pathKey = resolvePathEnvKey(envMap);
|
||||
const currentValue = envMap[pathKey] || '';
|
||||
const normalizedEntry = path.resolve(nextEntry);
|
||||
const segments = currentValue
|
||||
.split(path.delimiter)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.filter((entry) => {
|
||||
try {
|
||||
return path.resolve(entry) !== normalizedEntry;
|
||||
} catch {
|
||||
return entry !== nextEntry;
|
||||
}
|
||||
});
|
||||
|
||||
envMap[pathKey] = [nextEntry, ...segments].join(path.delimiter);
|
||||
}
|
||||
|
||||
const exampleEnv = readEnvFile(envExamplePath);
|
||||
const localEnv = readEnvFile(envLocalPath);
|
||||
|
||||
const mergedEnv = {
|
||||
...readEnvFile(envExamplePath),
|
||||
...readEnvFile(envLocalPath),
|
||||
...exampleEnv,
|
||||
...localEnv,
|
||||
...process.env,
|
||||
};
|
||||
|
||||
const runtimeNodePath = existsSync(bundledNodePath)
|
||||
? bundledNodePath
|
||||
: process.execPath;
|
||||
const runtimeNpmCliPath = existsSync(bundledNpmCliPath)
|
||||
? bundledNpmCliPath
|
||||
: '';
|
||||
const runtimeNodeDir = path.dirname(runtimeNodePath);
|
||||
|
||||
mergedEnv.PROJECT_ROOT = mergedEnv.PROJECT_ROOT || repoRoot;
|
||||
mergedEnv.NODE_SERVER_ADDR = mergedEnv.NODE_SERVER_ADDR || ':8081';
|
||||
mergedEnv.NODE_SERVER_TARGET =
|
||||
mergedEnv.NODE_SERVER_TARGET || resolveServerTarget(mergedEnv.NODE_SERVER_ADDR);
|
||||
mergedEnv.SQLITE_PATH =
|
||||
mergedEnv.SQLITE_PATH || path.join(repoRoot, 'server-node', 'data', 'genarrative.sqlite');
|
||||
mergedEnv.DATABASE_URL =
|
||||
mergedEnv.DATABASE_URL || DEFAULT_DEV_DATABASE_URL;
|
||||
prependEnvPath(mergedEnv, runtimeNodeDir);
|
||||
mergedEnv.npm_config_scripts_prepend_node_path = 'true';
|
||||
|
||||
const exampleDatabaseUrl = `${exampleEnv.DATABASE_URL || ''}`.trim();
|
||||
const localDatabaseUrl = `${localEnv.DATABASE_URL || ''}`.trim();
|
||||
const processDatabaseUrl = `${process.env.DATABASE_URL || ''}`.trim();
|
||||
const hasExplicitDatabaseUrl =
|
||||
Boolean(processDatabaseUrl) ||
|
||||
(Boolean(localDatabaseUrl) && localDatabaseUrl !== exampleDatabaseUrl);
|
||||
|
||||
if (!hasExplicitDatabaseUrl) {
|
||||
const databaseProbeTarget = resolveDatabaseProbeTarget(mergedEnv.DATABASE_URL);
|
||||
if (databaseProbeTarget) {
|
||||
const isReachable = await checkTcpReachable(databaseProbeTarget);
|
||||
if (!isReachable) {
|
||||
console.warn(
|
||||
`[dev:node] PostgreSQL unavailable at ${databaseProbeTarget.host}:${databaseProbeTarget.port}; falling back to ${DEV_MEMORY_DATABASE_URL} for local dev.`,
|
||||
);
|
||||
console.warn(
|
||||
'[dev:node] Current session will use in-memory persistence only. Set DATABASE_URL in .env.local to restore PostgreSQL-backed runtime data.',
|
||||
);
|
||||
mergedEnv.DATABASE_URL = DEV_MEMORY_DATABASE_URL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[dev:node] PROJECT_ROOT=${mergedEnv.PROJECT_ROOT}`);
|
||||
console.log(`[dev:node] NODE_SERVER_ADDR=${mergedEnv.NODE_SERVER_ADDR}`);
|
||||
console.log(`[dev:node] NODE_SERVER_TARGET=${mergedEnv.NODE_SERVER_TARGET}`);
|
||||
console.log(`[dev:node] SQLITE_PATH=${mergedEnv.SQLITE_PATH}`);
|
||||
console.log(`[dev:node] DATABASE_URL=${redactDatabaseUrl(mergedEnv.DATABASE_URL)}`);
|
||||
console.log(`[dev:node] NODE_RUNTIME=${runtimeNodePath}`);
|
||||
|
||||
const children = new Set();
|
||||
let shuttingDown = false;
|
||||
@@ -167,15 +298,27 @@ function registerChild(name, child, siblingProvider) {
|
||||
});
|
||||
}
|
||||
|
||||
const serverProcess = spawn(npmCommand, ['run', 'dev'], {
|
||||
cwd: serverRoot,
|
||||
env: mergedEnv,
|
||||
shell: process.platform === 'win32',
|
||||
stdio: 'inherit',
|
||||
});
|
||||
const serverProcess = existsSync(serverTsxCliPath)
|
||||
? spawn(runtimeNodePath, [serverTsxCliPath, 'watch', 'src/server.ts'], {
|
||||
cwd: serverRoot,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
: runtimeNpmCliPath
|
||||
? spawn(runtimeNodePath, [runtimeNpmCliPath, 'run', 'dev'], {
|
||||
cwd: serverRoot,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
: spawn(npmCommand, ['run', 'dev'], {
|
||||
cwd: serverRoot,
|
||||
env: mergedEnv,
|
||||
shell: process.platform === 'win32',
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
const viteProcess = spawn(
|
||||
process.execPath,
|
||||
runtimeNodePath,
|
||||
[viteCliPath, '--port=3000', '--host=0.0.0.0'],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
|
||||
Reference in New Issue
Block a user