nx
Version:
238 lines (237 loc) • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getForceColorForChild = getForceColorForChild;
exports.getEnvVariablesForBatchProcess = getEnvVariablesForBatchProcess;
exports.getTaskSpecificEnv = getTaskSpecificEnv;
exports.getGraphTimeDotEnvForTask = getGraphTimeDotEnvForTask;
exports.getEnvVariablesForTask = getEnvVariablesForTask;
exports.loadAndExpandDotEnvFile = loadAndExpandDotEnvFile;
exports.unloadDotEnvFile = unloadDotEnvFile;
exports.getEnvFilesForTask = getEnvFilesForTask;
const dotenv_1 = require("dotenv");
const dotenv_expand_1 = require("dotenv-expand");
const node_path_1 = require("node:path");
const workspace_root_1 = require("../utils/workspace-root");
const task_env_paths_1 = require("./task-env-paths");
/**
* Resolves the FORCE_COLOR value for forked child processes.
*
* When the user sets FORCE_COLOR=0, bin/nx.ts deletes it from process.env
* (workaround for picocolors treating "0" as truthy) and saves the original
* value in NX_ORIGINAL_FORCE_COLOR. Without this check, the undefined
* FORCE_COLOR would default to 'true', re-enabling colors in all children.
*/
function getForceColorForChild() {
if (process.env.FORCE_COLOR !== undefined) {
return process.env.FORCE_COLOR;
}
if (process.env.NX_ORIGINAL_FORCE_COLOR === '0') {
return '0';
}
return 'true';
}
function getEnvVariablesForBatchProcess(skipNxCache, captureStderr) {
const res = {
// User Process Env Variables override Dotenv Variables
...process.env,
// Nx Env Variables overrides everything
...getNxEnvVariablesForForkedProcess(getForceColorForChild(), skipNxCache, captureStderr),
};
// NX_ORIGINAL_FORCE_COLOR is an internal signal and should not leak into child processes
delete res.NX_ORIGINAL_FORCE_COLOR;
return res;
}
// The orchestrator now calls this eagerly during the coordinator pre-hash
// in addition to processTask (and again in hashBatchTasks), so the same
// task hits this function multiple times per run. Each call reads 3+ .env
// files from disk — memoize by task.id to skip the repeat work.
//
// Cache lifetime is the current Nx invocation: the function is only called
// from CLI/orchestrator code (not the long-lived daemon), so the map is
// scoped to a single run. Callers must not mutate the returned env — they
// already spread it into new objects before adding task-specific overrides
// (see getEnvVariablesForTask).
const taskSpecificEnvCache = new Map();
function getTaskSpecificEnv(task, graph) {
const cached = taskSpecificEnvCache.get(task.id);
if (cached)
return cached;
// Unload any dot env files at the root of the workspace that were loaded on init of Nx.
const taskEnv = unloadDotEnvFiles({ ...process.env });
const env = process.env.NX_LOAD_DOT_ENV_FILES === 'true'
? loadDotEnvFilesForTask(task, graph, taskEnv)
: // If not loading dot env files, ensure env vars created by system are still loaded
taskEnv;
taskSpecificEnvCache.set(task.id, env);
return env;
}
/**
* Reconstructs, at graph-construction time, the env a task would see: the
* root dotenv files Nx loaded at init are unloaded from the ambient env, then
* the dotenv files the task would load (`.env.<target>`, project-scoped
* `.env`, ...) are applied, so a task-scoped file wins over an init-time root
* load. The unload compares values, not provenance: a variable whose value
* differs from the root file's is kept and wins over the task files, while a
* shell-set value equal to the root file's is unloaded like the file's own.
*
* `createNodes` runs before any task, so the per-task dotenv files that
* `getTaskSpecificEnv` loads at run time are not in `process.env` yet. A plugin
* inferring targets from a config that reads `process.env` needs those values
* to resolve the config the way the task will. This mirrors
* `loadDotEnvFilesForTask` but takes the target coordinates directly (there is
* no `Task`/graph yet) and gates on `!== 'false'` rather than `=== 'true'`: the
* `'true'` marker is only stamped once the graph exists (`run-command.ts`),
* which is after this runs. Only the dotenv overlay is reconstructed: run-time
* env like `NX_TASK_TARGET_*` or `NX_TASK_HASH` is not included.
*
* `baseEnv` is the ambient env the overlay applies to, defaulting to the live
* `process.env`. A caller that runs config files in-process passes a snapshot
* taken under its load lock instead: a concurrent load's transient env writes
* would otherwise be read as ambient and mask the task files' values.
*/
function getGraphTimeDotEnvForTask(projectRoot, target, configuration, nonAtomizedTarget, baseEnv = process.env) {
// Unload the root dotenv files loaded on init of Nx so the task-scoped files win.
const env = unloadDotEnvFiles({ ...baseEnv });
// Fall back to the live env when the snapshot has no entry: Windows resolves
// `process.env` case-insensitively but a plain snapshot keeps whichever
// spelling was exported, so a lowercase opt-out is invisible here otherwise.
if ((baseEnv.NX_LOAD_DOT_ENV_FILES ?? process.env.NX_LOAD_DOT_ENV_FILES) ===
'false') {
return env;
}
const dotEnvFiles = (0, task_env_paths_1.getEnvPathsForTask)(projectRoot, target, configuration, nonAtomizedTarget);
loadAndExpandDotEnvFile(dotEnvFiles.map((file) => (0, node_path_1.join)(workspace_root_1.workspaceRoot, file)), env);
return env;
}
function getEnvVariablesForTask(task, taskSpecificEnv, forceColor, skipNxCache, captureStderr, outputPath, streamOutput) {
const res = {
// Start With Dotenv Variables
...taskSpecificEnv,
// Nx Env Variables overrides everything
...getNxEnvVariablesForTask(task, forceColor, skipNxCache, captureStderr, outputPath, streamOutput),
};
// we have to delete it because if we invoke Nx from within Nx, we need to reset those values
if (!outputPath) {
delete res.NX_TERMINAL_OUTPUT_PATH;
delete res.NX_STREAM_OUTPUT;
delete res.NX_PREFIX_OUTPUT;
}
// we don't reset NX_BASE or NX_HEAD because those are set by the user and should be preserved
delete res.NX_SET_CLI;
// NX_ORIGINAL_FORCE_COLOR is an internal signal used by getForceColorForChild()
// and should not leak into child processes
delete res.NX_ORIGINAL_FORCE_COLOR;
return res;
}
function getNxEnvVariablesForForkedProcess(forceColor, skipNxCache, captureStderr, outputPath, streamOutput) {
const env = {
FORCE_COLOR: forceColor,
NX_WORKSPACE_ROOT: workspace_root_1.workspaceRoot,
NX_SKIP_NX_CACHE: skipNxCache ? 'true' : undefined,
};
if (outputPath) {
env.NX_TERMINAL_OUTPUT_PATH = outputPath;
if (captureStderr) {
env.NX_TERMINAL_CAPTURE_STDERR = 'true';
}
if (streamOutput) {
env.NX_STREAM_OUTPUT = 'true';
}
}
return env;
}
function getNxEnvVariablesForTask(task, forceColor, skipNxCache, captureStderr, outputPath, streamOutput) {
const env = {
NX_TASK_TARGET_PROJECT: task.target.project,
NX_TASK_TARGET_TARGET: task.target.target,
NX_TASK_TARGET_CONFIGURATION: task.target.configuration ?? undefined,
NX_TASK_HASH: task.hash,
// used when Nx is invoked via Lerna
LERNA_PACKAGE_NAME: task.target.project,
};
// TODO: remove this once we have a reasonable way to configure it
if (task.target.target === 'test') {
env.NX_TERMINAL_CAPTURE_STDERR = 'true';
}
// Pass the root Nx process PID to nested processes for DB-based loop detection.
// The root PID is used as a key in the task_invocations table to track which tasks
// have been invoked across nested Nx processes.
return {
...getNxEnvVariablesForForkedProcess(forceColor, skipNxCache, captureStderr, outputPath, streamOutput),
...env,
// Ensure the TUI does not get spawned within the TUI if ever tasks invoke Nx again
NX_TUI: 'false',
// tracks the root PID for child nx tasks, used to verify nx is infinitely recursing through the same tasks
NX_INVOCATION_ROOT_PID: process.env.NX_INVOCATION_ROOT_PID ?? String(process.pid),
};
}
/**
* This function loads one or more .env files and expands the variables in them.
* When multiple files are provided, all files are loaded first, then variable
* expansion happens once with the complete set of variables. This ensures
* cross-file variable references resolve correctly.
* @param filename the .env file(s) to load
* @param environmentVariables the object to load environment variables into
* @param override whether to override existing environment variables
*/
function loadAndExpandDotEnvFile(filename, environmentVariables, override = false) {
const myEnv = (0, dotenv_1.config)({
path: filename,
processEnv: environmentVariables,
override,
});
return (0, dotenv_expand_1.expand)({
...myEnv,
processEnv: environmentVariables,
});
}
/**
* This function unloads a .env file and removes the variables in it from the environmentVariables.
* @param filename
* @param environmentVariables
*/
function unloadDotEnvFile(filename, environmentVariables, override = false) {
const parsedDotEnvFile = {};
loadAndExpandDotEnvFile(filename, parsedDotEnvFile, override);
Object.keys(parsedDotEnvFile).forEach((envVarKey) => {
if (environmentVariables[envVarKey] === parsedDotEnvFile[envVarKey]) {
delete environmentVariables[envVarKey];
}
});
}
function getOwnerTargetForTask(task, graph) {
const project = graph.nodes[task.target.project];
if (project.data.metadata?.targetGroups) {
for (const targets of Object.values(project.data.metadata.targetGroups)) {
if (targets.includes(task.target.target)) {
for (const target of targets) {
// a target group can name a target the project does not define —
// inference plugins build the group and the targets separately, so
// the two can disagree. skip the phantom rather than dereferencing it.
if (project.data.targets[target]?.metadata?.nonAtomizedTarget) {
return [
target,
project.data.targets[target]?.metadata?.nonAtomizedTarget,
];
}
}
}
}
}
return [task.target.target];
}
function getEnvFilesForTask(task, graph) {
const [target, nonAtomizedTarget] = getOwnerTargetForTask(task, graph);
return (0, task_env_paths_1.getEnvPathsForTask)(task.projectRoot, target, task.target.configuration, nonAtomizedTarget);
}
function loadDotEnvFilesForTask(task, graph, environmentVariables) {
const dotEnvFiles = getEnvFilesForTask(task, graph);
loadAndExpandDotEnvFile(dotEnvFiles.map((file) => (0, node_path_1.join)(workspace_root_1.workspaceRoot, file)), environmentVariables);
return environmentVariables;
}
function unloadDotEnvFiles(environmentVariables) {
for (const file of ['.env', '.local.env', '.env.local']) {
unloadDotEnvFile((0, node_path_1.join)(workspace_root_1.workspaceRoot, file), environmentVariables);
}
return environmentVariables;
}