feat(lanes): detect docker-compose database/Redis and wire ENV_REWRITE (A3)

This commit is contained in:
2026-08-04 09:13:20 +07:00
parent 0ecc2eec24
commit c4b11799dc
2 changed files with 173 additions and 5 deletions
+109 -5
View File
@@ -11,6 +11,7 @@
const fs = require("node:fs");
const path = require("node:path");
const yaml = require("js-yaml");
/**
* A name safe to embed as literal text inside a generated shell script (an
@@ -46,6 +47,97 @@ function pickScript(pkg, candidates) {
return null;
}
/**
* A repo-name-derived DB_PREFIX: lowercase alnum + underscore, "_l" suffix
* (lane N's actual name becomes "<prefix>N"), matching the convention in
* the postgres-compose profile template's example.
*
* @param {string} repoPath - Absolute path to the repository root.
* @returns {string}
*/
function dbPrefixFor(repoPath) {
const base = path
.basename(repoPath)
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return `${base || "app"}_l`;
}
/**
* The first docker-compose service name matching `pattern`, or null. Reads
* `docker-compose.yml`/`.yaml` at the repo root; never executes it.
*
* @param {string} repoPath - Absolute path to the repository root.
* @param {RegExp} pattern - Pattern to match service name.
* @returns {string|null}
*/
function findComposeService(repoPath, pattern) {
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
let doc;
try {
doc = yaml.load(fs.readFileSync(path.join(repoPath, name), "utf8"));
} catch {
continue;
}
const services = doc && typeof doc.services === "object" ? doc.services : {};
for (const serviceName of Object.keys(services)) {
if (pattern.test(serviceName) && IDENTIFIER_RE.test(serviceName)) return serviceName;
}
}
return null;
}
/**
* Where a lane's .env lives for this layout: backend/.env in a monorepo,
* .env at the root for single-service. Returns the source to seed FROM
* (real .env, else .env.example) or null when neither exists.
*
* @param {string} repoPath - Absolute path to the repository root.
* @param {string} envDir - Directory where .env lives (e.g. "backend" or ".").
* @returns {object|null}
*/
function findEnvSource(repoPath, envDir) {
const dir = path.join(repoPath, envDir);
const real = path.join(dir, ".env");
const example = path.join(dir, ".env.example");
const relFile = path.join(envDir, ".env").replace(/\\/g, "/").replace(/^\.\//, "");
if (fs.existsSync(real)) return { file: relFile, source: relFile, fromExample: false };
if (fs.existsSync(example)) {
return { file: relFile, source: `${relFile}.example`, fromExample: true };
}
return null;
}
/**
* Detect database and Redis services from docker-compose.yml and wire up
* .env rewriting if services are found.
*
* @param {string} repoPath - Absolute path to the repository root.
* @param {string} envDir - Directory where .env lives (e.g. "backend" or ".").
* @returns {object}
*/
function detectDatabaseAndEnv(repoPath, envDir) {
const dbService = findComposeService(repoPath, /postgres/i);
const redis = findComposeService(repoPath, /redis/i) !== null;
const database = dbService
? { dbPrefix: dbPrefixFor(repoPath), dbService, dbKind: "postgres" }
: null;
let env = null;
if (database || redis) {
const found = findEnvSource(repoPath, envDir);
if (found) {
const rewrite = [];
if (database) rewrite.push("DATABASE_URL");
if (redis) rewrite.push("REDIS_URL");
env = { dir: envDir, ...found, rewrite };
}
}
return { database, redis, env };
}
/**
* Detect a Node.js project's layout and, for each service, which npm script
* to run. Returns null when neither supported layout matches — the caller
@@ -59,28 +151,40 @@ function detectNode(repoPath) {
const frontendPkg = readPackageJson(path.join(repoPath, "frontend"));
if (backendPkg && frontendPkg) {
const { database, redis, env } = detectDatabaseAndEnv(repoPath, "backend");
return {
layout: "monorepo",
services: {
backend: { dir: "backend", script: pickScript(backendPkg, ["start", "dev"]) },
frontend: { dir: "frontend", script: pickScript(frontendPkg, ["preview", "dev"]) },
},
database: null,
env: null,
database,
redis,
env,
};
}
const rootPkg = readPackageJson(repoPath);
if (rootPkg) {
const { database, redis, env } = detectDatabaseAndEnv(repoPath, ".");
return {
layout: "single-service",
services: { app: { dir: ".", script: pickScript(rootPkg, ["start", "dev"]) } },
database: null,
env: null,
database,
redis,
env,
};
}
return null;
}
module.exports = { IDENTIFIER_RE, readPackageJson, pickScript, detectNode };
module.exports = {
IDENTIFIER_RE,
readPackageJson,
pickScript,
dbPrefixFor,
findComposeService,
findEnvSource,
detectNode,
};