nx
Version:
423 lines (422 loc) • 15.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.hashDaemonClientEnv = hashDaemonClientEnv;
exports.getDaemonEnv = getDaemonEnv;
exports.getDaemonClientEnvGeneration = getDaemonClientEnvGeneration;
exports.getDaemonSpawnEnv = getDaemonSpawnEnv;
exports.getAppliedDaemonClientEnv = getAppliedDaemonClientEnv;
exports.applyDaemonEnvFromClient = applyDaemonEnvFromClient;
exports.normalizeDaemonEnvironmentForGraph = normalizeDaemonEnvironmentForGraph;
exports.getChangedEnvKeys = getChangedEnvKeys;
const file_hasher_1 = require("../../hasher/file-hasher");
const DAEMON_ENV_REQUIRED_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false',
NX_CACHE_PROJECTS_CONFIG: 'false',
};
const DAEMON_ENV_OVERRIDABLE_SETTINGS = {
NX_VERBOSE_LOGGING: 'true',
NX_PERF_LOGGING: 'true',
NX_NATIVE_LOGGING: 'nx=debug',
};
/**
* Env vars that should NOT be sent to the daemon because they cannot affect
* the project graph. Only vars that can actually affect the project graph
* (e.g. PATH, JAVA_HOME, GRADLE_HOME) should be allowed through.
*/
const DAEMON_ENV_VARS_EXCLUSIONS = new Set([
// Nx task-scoped vars
'NX_TASK_TARGET_CONFIGURATION',
'NX_TASK_TARGET_PROJECT',
'NX_TASK_TARGET_TARGET',
'NX_TASK_HASH',
'NX_TERMINAL_OUTPUT_PATH',
'NX_TERMINAL_CAPTURE_STDERR',
'NX_STREAM_OUTPUT',
'NX_PREFIX_OUTPUT',
'NX_FORKED_TASK_EXECUTOR',
'NX_SET_CLI',
'NX_INVOKED_BY_RUNNER',
// NX_LOAD_DOT_ENV_FILES is intentionally NOT excluded: it must reach the
// daemon so graph-time dotenv resolution honors the user's opt-out. It is
// sent normalized rather than reflected — see normalizedLoadDotEnvFiles.
'NX_SKIP_NX_CACHE',
'NX_CACHE_FAILURES',
'NX_REJECT_UNKNOWN_LOCAL_CACHE',
'NX_IGNORE_CYCLES',
'NX_BATCH_MODE',
'NX_CI_EXECUTION_ID',
'NX_DAEMON_PROCESS',
'NX_CLI_SET',
// Set by Nx Console on the extension host; always equals the daemon's own
// workspace root (foreign-workspace messages are rejected), and the daemon
// resolves its root at startup before any client env is applied. The spawn
// env keeps it so that startup resolution honors the pinned root.
'NX_WORKSPACE_ROOT_PATH',
// The message-size ceiling is read when a connection is accepted, before the
// client's env could apply, so reflecting it would hand one client's value
// to the next client's connection. Pinned at daemon spawn instead; see
// getDaemonSpawnEnv.
'NX_MAX_MESSAGE_SIZE',
// Nx UI/logging vars (don't affect graph structure)
'NX_TUI',
'NX_TUI_AUTO_EXIT',
'NX_TUI_SKIP_CAPABILITY_CHECK',
'NX_VERBOSE_LOGGING',
'NX_PERF_LOGGING',
'NX_NATIVE_LOGGING',
'NX_PROFILE',
'NX_DAEMON_VERBOSE_LOGGING',
'NX_ORIGINAL_FORCE_COLOR',
// AI agent detection vars (the daemon itself is not an AI agent)
'CLAUDECODE',
'CLAUDE_CODE',
'REPL_ID',
'CURSOR_TRACE_ID',
'COMPOSER_NO_INTERACTION',
'OPENCODE',
'CODEX_THREAD_ID',
'SUPERSET_AGENT_ID',
'GEMINI_CLI',
'COPILOT_CLI',
'AI_AGENT',
// Set per session or per release alongside the detection vars above, so they
// churn the daemon's environment across sessions of the same agent. Named
// rather than prefix-matched: COPILOT_HOME and COPILOT_GITHUB_TOKEN share the
// prefix and are kept on purpose, and CLAUDE_PID is not under CLAUDE_CODE_.
'COPILOT_AGENT_SESSION_ID',
'COPILOT_CLI_BINARY_VERSION',
'CLAUDE_PID',
// Shell mechanics
'_',
'SHLVL',
'PWD',
'OLDPWD',
'SHELL_SESSION_ID',
'TERM_SESSION_ID',
'SECURITYSESSIONID',
'COMMAND_MODE',
'WINDOWID',
'COLUMNS',
'LINES',
'TMPDIR',
'PROMPT',
// Package-manager per-invocation vars not covered by the npm_/pnpm_ prefixes
'INIT_CWD',
'COLOR',
// Per-shell-session state; these tools' other vars are stable or can be
// meaningful (e.g. fnm's FNM_DIR), so no prefix exclusion
'FNM_MULTISHELL_PATH',
'POSH_SESSION_ID',
'MISE_SHELL',
// Editor preference; graph computation is non-interactive
'EDITOR',
'VISUAL',
// Output presentation
'PAGER',
'FORCE_COLOR',
'NO_COLOR',
'NODE_DISABLE_COLORS',
'FORCE_HYPERLINK',
// Terminal identification/presentation; differs per terminal app, so it
// churns whenever clients from different terminals share the daemon
'TERM',
'TERM_PROGRAM',
'TERM_PROGRAM_VERSION',
'COLORTERM',
'COLORFGBG',
'VTE_VERSION',
'GNOME_TERMINAL_SCREEN',
'GNOME_TERMINAL_SERVICE',
'WT_SESSION',
'WT_PROFILE_ID',
'TERMINAL_EMULATOR',
'TERMINUS_SUBLIME',
'ConEmuTask',
'ZELLIJ_PANE_ID',
'LC_TERMINAL',
'LC_TERMINAL_VERSION',
// Injected by VS Code and its extensions into integrated terminals. Single
// entries rather than GIT_/CHROME_/COPILOT_ prefixes: GIT_DIR, CHROME_BIN,
// COPILOT_HOME and COPILOT_GITHUB_TOKEN are functional inputs.
'GIT_ASKPASS',
'GIT_EDITOR',
'GIT_PAGER',
'GIT_MERGE_AUTOEDIT',
'APPLICATION_INSIGHTS_NO_STATSBEAT',
// Override for the @microsoft/mxc-sdk sandbox binaries; the SDK falls back
// to its bundled binaries when unset
'MXC_BIN_DIR',
'CHROME_CRASHPAD_PIPE_NAME',
'COPILOT_OTEL_FILE_EXPORTER_PATH',
'COPILOT_DEBUG_NONCE',
// Session / auth
'SSH_AUTH_SOCK',
'SSH_AGENT_PID',
'XDG_SESSION_ID',
'DBUS_SESSION_BUS_ADDRESS',
'DISPLAY',
// macOS internals
'__CF_USER_TEXT_ENCODING',
'__CFBundleIdentifier',
]);
/**
* Env var prefixes that should never be sent to the daemon.
* These cover CI platforms, package managers, editors, and terminals.
*/
const DAEMON_ENV_PREFIX_EXCLUSIONS = [
// CI platforms
'GITHUB_',
'RUNNER_',
'CI_JOB_',
'CI_PIPELINE_',
'CIRCLE_',
'JENKINS_',
'BUILD_',
'AGENT_',
'SYSTEM_TASK',
// Package managers (process-scoped)
'npm_',
'pnpm_',
// Nx Cloud runner/agent vars (per-worker values like NX_CLOUD_WORKER_ID
// and NX_CLOUD_EXECUTION_ID diverge between distributed-execution workers
// and cannot affect the project graph; without this every worker that
// talks to the daemon would force a graph recompute). The cloud auth/access
// tokens are allow-listed below — they are stable and signal cloud usage.
'NX_CLOUD_',
// Editors / IDEs
'VSCODE_',
'JETBRAINS_',
'ELECTRON_',
// AI agent harnesses (per-session ids; the bare CLAUDECODE/CLAUDE_CODE
// detection vars are excluded above)
'CLAUDE_CODE_',
// Shell prompts / integrations (per-command and per-session state)
'ATUIN_',
'STARSHIP_',
'MCFLY_',
'DIRENV_',
'__MISE_',
// Terminal emulators
'ITERM_',
'KITTY_',
'WEZTERM_',
'ALACRITTY_',
'KONSOLE_',
'GHOSTTY_',
'TMUX',
// Benchmarking / profiling tools
'HYPERFINE_', // hyperfine sets HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET for each iteration
];
/**
* Env var name patterns that should never be sent to the daemon. For vars
* whose NAME embeds a process id, so neither an exact entry nor a safe
* prefix can match them.
*/
const DAEMON_ENV_PATTERN_EXCLUSIONS = [
// Set by Windows 11 explorer.exe as EFC_<pid> (observed as EFC_<pid>_<hash>
// in Electron-spawned shells); regenerated on every login
/^EFC_\d+(_\d+)?$/,
];
/**
* Vars that match an excluded prefix but should still reach the daemon. The
* Nx Cloud auth/access tokens are excluded by the `NX_CLOUD_` prefix, but
* unlike the per-worker cloud vars they are stable across clients (so they
* don't churn the daemon env) and signal that the workspace uses Nx Cloud
* (e.g. for analytics). Keep them.
*/
const DAEMON_ENV_PREFIX_EXCLUSION_OVERRIDES = new Set([
'NX_CLOUD_ACCESS_TOKEN',
'NX_CLOUD_AUTH_TOKEN',
]);
function isExcludedEnvVar(key) {
if (DAEMON_ENV_VARS_EXCLUSIONS.has(key) ||
DAEMON_ENV_PATTERN_EXCLUSIONS.some((pattern) => pattern.test(key))) {
return true;
}
if (DAEMON_ENV_PREFIX_EXCLUSION_OVERRIDES.has(key)) {
return false;
}
return DAEMON_ENV_PREFIX_EXCLUSIONS.some((prefix) => key.startsWith(prefix));
}
/**
* Digest of the client-controlled portion of the daemon env: the vars
* `getDaemonEnv` would send, minus the required settings the daemon pins
* itself. Skipping those yields the same digest whether the process is a
* daemon plugin worker (which has them set) or a daemonless one (which
* typically does not).
*/
function hashDaemonClientEnv() {
const env = {};
for (const key in process.env) {
if (!isExcludedEnvVar(key) &&
!Object.hasOwn(DAEMON_ENV_REQUIRED_SETTINGS, key)) {
env[key] = process.env[key];
}
}
env.NX_LOAD_DOT_ENV_FILES = normalizedLoadDotEnvFiles();
return (0, file_hasher_1.hashObject)(env);
}
/**
* `NX_LOAD_DOT_ENV_FILES` as its meaning rather than its spelling. `run-command`
* stamps `'true'` once the graph exists, so a task child sends it where its
* parent sent nothing; every reader outside the task runner treats both alike
* (`!== 'false'`). Reflecting the spelling would flip the daemon env on each
* alternation and discard the graph cache both ways.
*/
function normalizedLoadDotEnvFiles() {
return process.env.NX_LOAD_DOT_ENV_FILES === 'false' ? 'false' : 'true';
}
function getDaemonEnv() {
const env = { ...DAEMON_ENV_OVERRIDABLE_SETTINGS };
for (const key in process.env) {
if (!isExcludedEnvVar(key)) {
env[key] = process.env[key];
}
}
env.NX_LOAD_DOT_ENV_FILES = normalizedLoadDotEnvFiles();
return Object.assign(env, DAEMON_ENV_REQUIRED_SETTINGS);
}
let clientEnvGeneration = 0;
let clientEnvApplySequence = 0;
let appliedClientEnv;
/**
* Count of client env applications that changed at least one variable, in
* `process.env` or against the previously applied client env. Digest equality
* alone cannot guard a cache write: an env that changed and changed back
* mid-pass yields the pass-start digest again, while the count still moves.
*/
function getDaemonClientEnvGeneration() {
return clientEnvGeneration;
}
/**
* Env for spawning the daemon process. On top of the reflected env, it must
* keep excluded vars the daemon needs to start correctly:
* - ELECTRON_RUN_AS_NODE (matched by the ELECTRON_ prefix exclusion): when
* the spawning client runs inside an Electron host, process.execPath is
* the Electron binary and only this var makes it run the daemon's Node
* entry point.
* - NX_WORKSPACE_ROOT_PATH: the daemon resolves its workspace root at
* startup by walking up from cwd looking for workspace markers; without
* the pin, a root without markers under an ancestor that has them
* resolves to the ancestor and the daemon publishes its socket under the
* wrong workspace.
* - NX_MAX_MESSAGE_SIZE: excluded from reflection (see above), so the value
* here holds for the daemon's whole lifetime. Changing it therefore needs a
* daemon restart (`nx reset`).
*/
function getDaemonSpawnEnv() {
const env = getDaemonEnv();
if (process.env.ELECTRON_RUN_AS_NODE !== undefined) {
env.ELECTRON_RUN_AS_NODE = process.env.ELECTRON_RUN_AS_NODE;
}
if (process.env.NX_WORKSPACE_ROOT_PATH !== undefined) {
env.NX_WORKSPACE_ROOT_PATH = process.env.NX_WORKSPACE_ROOT_PATH;
}
if (process.env.NX_MAX_MESSAGE_SIZE !== undefined) {
env.NX_MAX_MESSAGE_SIZE = process.env.NX_MAX_MESSAGE_SIZE;
}
return env;
}
/**
* A copy of the env the last `applyDaemonEnvFromClient` call applied, with a
* sequence that advances on every call, or `undefined` before the first. For a
* caller that let code it ran (a user config, say) write over `process.env`
* and has to put the client's env back: the generation cannot tell it an apply
* happened, since an apply whose values the config had already written changes
* nothing.
*/
function getAppliedDaemonClientEnv() {
return appliedClientEnv
? { sequence: clientEnvApplySequence, env: { ...appliedClientEnv } }
: undefined;
}
/**
* Without the deletion step, a var set by one client (e.g.
* `NX_PREFER_NODE_STRIP_TYPES=true` or `JAVA_TOOL_OPTIONS=...` for a single
* command) would persist in the daemon and leak into every subsequent
* client's project-graph computation. Deletion skips excluded vars and
* required settings, which the daemon owns and clients should not control.
*
* The returned keys are those `process.env` moved on plus those the client's
* env moved on since the last applied one: a value a config wrote mid-load can
* already match what the next client sends, and the graph computed under the
* previous client is stale all the same.
*/
function applyDaemonEnvFromClient(newEnv) {
const changedKeys = new Set();
const allKeys = new Set([
...Object.keys(process.env),
...Object.keys(newEnv),
]);
for (const key of allKeys) {
if (key in newEnv) {
if (process.env[key] !== newEnv[key]) {
process.env[key] = newEnv[key];
changedKeys.add(key);
}
}
else if (!isExcludedEnvVar(key) &&
!Object.hasOwn(DAEMON_ENV_REQUIRED_SETTINGS, key)) {
delete process.env[key];
changedKeys.add(key);
}
}
if (appliedClientEnv) {
for (const key of new Set([
...Object.keys(appliedClientEnv),
...Object.keys(newEnv),
])) {
if (appliedClientEnv[key] !== newEnv[key]) {
changedKeys.add(key);
}
}
}
if (changedKeys.size > 0) {
clientEnvGeneration++;
}
clientEnvApplySequence++;
appliedClientEnv = { ...newEnv };
return [...changedKeys];
}
const BERRY_BIN_FOLDER_GRAPH_IDENTITY = '<YARN_BERRY_BIN_FOLDER>';
/**
* Yarn Berry creates a fresh BERRY_BIN_FOLDER for every invocation and puts it
* first on PATH. The wrappers inside are runtime state the daemon and workers
* must receive, but the folder's random name is not project-graph identity:
* two invocations whose environments differ only by it would compute the same
* graph. Strips the folder from PATH and pins the var to a sentinel so such
* environments compare equal. Returns a copy; never mutates runtime env.
*/
function normalizeDaemonEnvironmentForGraph(env, platform = process.platform) {
const normalized = { ...env };
const berryBinFolder = normalized.BERRY_BIN_FOLDER;
if (!berryBinFolder) {
return normalized;
}
const pathKey = Object.keys(normalized).find((key) => key.toUpperCase() === 'PATH');
if (pathKey && normalized[pathKey]) {
const pathDelimiter = platform === 'win32' ? ';' : ':';
const canonicalize = (value) => {
const withPlatformSeparators = platform === 'win32' ? value.replaceAll('/', '\\') : value;
return platform === 'win32'
? withPlatformSeparators.toLowerCase()
: withPlatformSeparators;
};
const canonicalBerryBinFolder = canonicalize(berryBinFolder);
normalized[pathKey] = normalized[pathKey]
.split(pathDelimiter)
.filter((entry) => canonicalize(entry) !== canonicalBerryBinFolder)
.join(pathDelimiter);
}
normalized.BERRY_BIN_FOLDER = BERRY_BIN_FOLDER_GRAPH_IDENTITY;
return normalized;
}
function getChangedEnvKeys(before, after) {
const changedKeys = [];
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
if (before[key] !== after[key]) {
changedKeys.push(key);
}
}
return changedKeys;
}