UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

238 lines (237 loc) 11.5 kB
"use strict"; 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; }