Skip to content

Commit 3d9bc7a

Browse files
barbados-clemensclaudenx-cloud[bot]
authored
docs(repo): add conformance rule for NX_* env var documentation (#35353)
## Current Behavior `NX_*` env vars can be added to Nx source without being documented in `astro-docs/src/content/docs/reference/environment-variables.mdoc`. Nothing catches the drift. ## Expected Behavior Adds a conformance rule (`env-vars-documented`) that fails when an `NX_*` var is read in source but missing from the docs. Covers TS/JS `process.env.NX_*` and Rust `env::var` / `env!` patterns, skipping tests and fixtures. An `ignore` list in `nx.json` handles internal markers not meant to be documented. Also documents the user-facing vars the first run surfaced (self-hosted cache, provenance, plugin isolation, Nx Cloud timeouts, etc.) and marks `NX_CLOUD_AUTH_TOKEN` and `NX_CLOUD_DISTRIBUTED_EXECUTION_AGENT_COUNT` as deprecated. ## Related Issue(s) N/A — internal tooling improvement. --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com> Co-authored-by: barbados-clemens <[email protected]>
1 parent 6c159a4 commit 3d9bc7a

7 files changed

Lines changed: 366 additions & 64 deletions

File tree

astro-docs/src/content/docs/concepts/nx-daemon.mdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ To see information about the running Nx Daemon (such as its background process I
4949
## Customizing the socket location
5050

5151
The Nx Daemon uses a unix socket to communicate between the daemon and the Nx processes. By default this socket gets placed in a temp directory. If you are using Nx in a docker-compose environment, however, you may want to run the daemon manually
52-
and control its location to enable sharing the daemon among your docker containers. To do so, set the `NX_DAEMON_SOCKET_DIR` environment variable to a shared directory.
52+
and control its location to enable sharing the daemon among your docker containers. To do so, set the `NX_SOCKET_DIR` environment variable to a shared directory.
5353

5454
## Daemon behavior in containers
5555

astro-docs/src/content/docs/reference/environment-variables.mdoc

Lines changed: 89 additions & 58 deletions
Large diffs are not rendered by default.

astro-docs/src/content/docs/troubleshooting/nx-sandbox-unix-sockets.mdoc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ Both environment variables work around the symptom rather than fixing the root c
6464

6565
For advanced use cases, Nx provides environment variables to override where socket files are created:
6666

67-
| Variable | Description |
68-
| -------------------------------- | ------------------------------------------------------------------ |
69-
| `NX_SOCKET_DIR` | Overrides the directory for both daemon and forked process sockets |
70-
| `NX_DAEMON_SOCKET_DIR` | Overrides the daemon socket directory specifically |
71-
| `NX_NATIVE_FILE_CACHE_DIRECTORY` | Overrides the native file cache path |
67+
| Variable | Description |
68+
| -------------------------------- | --------------------------------------------------------------------------------------------------- |
69+
| `NX_SOCKET_DIR` | Overrides the directory for all Nx sockets (daemon, forked process, and plugin) |
70+
| `NX_DAEMON_SOCKET_DIR` | Legacy alias of `NX_SOCKET_DIR`. Used only when `NX_SOCKET_DIR` is not set. Prefer `NX_SOCKET_DIR`. |
71+
| `NX_NATIVE_FILE_CACHE_DIRECTORY` | Overrides the native file cache path |
7272

7373
These are primarily useful for environments with restricted temp directory access (e.g., certain Docker or CI setups). For Claude Code's sandbox, `allowAllUnixSockets: true` is simpler and more reliable.

nx.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,40 @@
376376
},
377377
{
378378
"rule": "./dist/workspace-plugin/src/conformance-rules/types-versions-exports-sync"
379+
},
380+
{
381+
"rule": "./dist/workspace-plugin/src/conformance-rules/env-vars-documented",
382+
"options": {
383+
"ignore": [
384+
"NX_ANALYTICS_SESSION_ID",
385+
"NX_FORKED_TASK_EXECUTOR",
386+
"NX_RUN_COMMANDS_DIRECTLY",
387+
"NX_DAEMON_PROCESS",
388+
"NX_IPC_CHANNEL_ID",
389+
"NX_PSEUDO_TERMINAL_EXEC_ARGV",
390+
"NX_GENERATE_DOCS_PROCESS",
391+
"NX_RELEASE_INTERNAL_SUPPRESS_FILTER_LOG",
392+
"NX_WORKSPACE_ROOT_PATH",
393+
"NX_IMPORT_SOURCE",
394+
"NX_IMPORT_DESTINATION",
395+
"NX_PROJECT_GRAPH_CACHE_DIRECTORY",
396+
"NX_CONSOLE",
397+
"NX_DEBUG_TELEMETRY",
398+
"NX_TUI_INLINE_MODE",
399+
"NX_TUI_SKIP_CAPABILITY_CHECK",
400+
"NX_SKIP_CHECK_REMOTE",
401+
"NX_CLI_SET",
402+
"NX_WORKSPACE_ROOT",
403+
"NX_TERMINAL_OUTPUT_PATH",
404+
"NX_TERMINAL_CAPTURE_STDERR",
405+
"NX_VERSION",
406+
"NX_STREAM_OUTPUT",
407+
"NX_PREFIX_OUTPUT",
408+
"NX_INFER_ALL_PACKAGE_JSONS",
409+
"NX_AI_FILES_USE_LOCAL",
410+
"NX_WINDOWS_PTY_SUPPORT"
411+
]
412+
}
379413
}
380414
]
381415
},
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { extractDocumentedVars, extractUsedVarsFromContent } from './index';
2+
3+
describe('env-vars-documented', () => {
4+
describe('extractDocumentedVars()', () => {
5+
it('extracts NX_* names from table rows', () => {
6+
const mdoc = [
7+
'## Nx environment variables',
8+
'',
9+
'| Property | Type | Description |',
10+
'| ------------------------ | ------- | ----------------- |',
11+
'| `NX_BAIL` | boolean | Stop on failure. |',
12+
'| `NX_DAEMON` | boolean | Disable daemon. |',
13+
'| `SOMETHING_ELSE` | string | Not an Nx var. |',
14+
'',
15+
].join('\n');
16+
17+
const result = extractDocumentedVars(mdoc);
18+
19+
expect(Array.from(result).sort()).toEqual(['NX_BAIL', 'NX_DAEMON']);
20+
});
21+
22+
it('ignores NX_* mentions in prose and descriptions', () => {
23+
const mdoc = [
24+
'Setting `NX_PROSE_ONLY` in your shell will not be detected here.',
25+
'| `NX_REAL_ENTRY` | boolean | Some `NX_INLINE_CODE` in the description. |',
26+
].join('\n');
27+
28+
const result = extractDocumentedVars(mdoc);
29+
30+
expect(Array.from(result)).toEqual(['NX_REAL_ENTRY']);
31+
});
32+
});
33+
34+
describe('extractUsedVarsFromContent()', () => {
35+
it('finds NX_* vars via process.env.X access', () => {
36+
const source = `
37+
if (process.env.NX_BAIL === 'true') return;
38+
const token = process.env.NX_CLOUD_ACCESS_TOKEN;
39+
`;
40+
expect(extractUsedVarsFromContent(source, 'foo.ts').sort()).toEqual([
41+
'NX_BAIL',
42+
'NX_CLOUD_ACCESS_TOKEN',
43+
]);
44+
});
45+
46+
it('finds NX_* vars via process.env["X"] and [\'X\'] access', () => {
47+
const source = `
48+
process.env["NX_DOUBLE_QUOTED"];
49+
process.env['NX_SINGLE_QUOTED'];
50+
process.env[\`NX_TEMPLATE_LITERAL\`];
51+
`;
52+
expect(extractUsedVarsFromContent(source, 'foo.ts').sort()).toEqual([
53+
'NX_DOUBLE_QUOTED',
54+
'NX_SINGLE_QUOTED',
55+
'NX_TEMPLATE_LITERAL',
56+
]);
57+
});
58+
59+
it('does not match string literals outside of process.env access', () => {
60+
const source = `
61+
const msg = "set NX_FAKE to true";
62+
const other = 'NX_ALSO_FAKE';
63+
`;
64+
expect(extractUsedVarsFromContent(source, 'foo.ts')).toEqual([]);
65+
});
66+
67+
it('finds NX_* vars in Rust via env::var and std::env::var', () => {
68+
const source = `
69+
let a = env::var("NX_FIRST");
70+
let b = std::env::var("NX_SECOND").unwrap_or_default();
71+
let c = env::var_os("NX_THIRD");
72+
`;
73+
expect(extractUsedVarsFromContent(source, 'foo.rs').sort()).toEqual([
74+
'NX_FIRST',
75+
'NX_SECOND',
76+
'NX_THIRD',
77+
]);
78+
});
79+
80+
it('does not flag Rust env! macro usage (compile-time)', () => {
81+
const source = `let build_tag = env!("NX_BUILD_TAG");`;
82+
expect(extractUsedVarsFromContent(source, 'foo.rs')).toEqual([]);
83+
});
84+
85+
it('finds NX_* vars in Rust via EnvFilter::(try_)?from_env', () => {
86+
const source = `
87+
EnvFilter::try_from_env("NX_TRY_FROM_ENV");
88+
EnvFilter::from_env("NX_FROM_ENV");
89+
`;
90+
expect(extractUsedVarsFromContent(source, 'foo.rs').sort()).toEqual([
91+
'NX_FROM_ENV',
92+
'NX_TRY_FROM_ENV',
93+
]);
94+
});
95+
96+
it('returns duplicates when the same var is read more than once', () => {
97+
const source = `
98+
process.env.NX_VERBOSE_LOGGING;
99+
if (process.env.NX_VERBOSE_LOGGING === 'true') {}
100+
`;
101+
expect(extractUsedVarsFromContent(source, 'foo.ts')).toEqual([
102+
'NX_VERBOSE_LOGGING',
103+
'NX_VERBOSE_LOGGING',
104+
]);
105+
});
106+
});
107+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
createConformanceRule,
3+
type ConformanceViolation,
4+
} from '@nx/conformance';
5+
import type { Tree } from '@nx/devkit';
6+
7+
type Options = {
8+
ignore?: string[];
9+
};
10+
11+
const DOCS_PATH =
12+
'astro-docs/src/content/docs/reference/environment-variables.mdoc';
13+
const NX_CORE_PROJECT = 'nx';
14+
const MAX_EXAMPLE_FILES = 3;
15+
16+
const SCANNABLE_EXT = /\.(ts|tsx|js|mjs|cjs|rs)$/;
17+
const TEST_FILE = /\.(spec|test)\.(ts|tsx|js)$/;
18+
const EXCLUDED_SEGMENT = /(__fixtures__|__snapshots__|\/files\/|\/dist\/)/;
19+
20+
const TS_JS_USAGE = /process\.env(?:\.|\[['"`])(NX_[A-Z0-9_]+)/g;
21+
const RUST_ENV_VAR = /(?:std::)?env::var(?:_os)?\s*\(\s*"(NX_[A-Z0-9_]+)"/g;
22+
const RUST_FROM_ENV = /(?:try_)?from_env\s*\(\s*"(NX_[A-Z0-9_]+)"/g;
23+
const DOCS_TABLE_ROW = /^\|\s*`(NX_[A-Z0-9_]+)`/gm;
24+
25+
export default createConformanceRule<Options>({
26+
name: 'env-vars-documented',
27+
category: 'consistency',
28+
description:
29+
'Ensures every NX_* environment variable in source code is covered by docs',
30+
implementation: async ({ tree, fileMapCache, ruleOptions }) => {
31+
const ignore = new Set(ruleOptions?.ignore ?? []);
32+
33+
const docsContent = tree.read(DOCS_PATH, 'utf-8');
34+
if (!docsContent) {
35+
return {
36+
severity: 'high',
37+
details: {
38+
violations: [
39+
{
40+
message: `Could not read ${DOCS_PATH}. The conformance rule expects to run from the workspace root.`,
41+
file: DOCS_PATH,
42+
},
43+
],
44+
},
45+
};
46+
}
47+
const documented = extractDocumentedVars(docsContent);
48+
49+
const nxFiles =
50+
fileMapCache.fileMap.projectFileMap?.[NX_CORE_PROJECT] ?? [];
51+
const filesToScan = nxFiles
52+
.map(({ file }) => file)
53+
.filter(isScannableSourceFile);
54+
const usages = extractUsagesFromFiles(tree, filesToScan);
55+
56+
const violations: ConformanceViolation[] = [];
57+
for (const [name, files] of usages) {
58+
if (documented.has(name) || ignore.has(name)) continue;
59+
const examples = [...files].join(', ');
60+
violations.push({
61+
message: `Env var \`${name}\` not documented. Found in ${examples}. Add a row to ${DOCS_PATH} or list \`${name}\` in the rule's "ignore" option.`,
62+
file: DOCS_PATH,
63+
});
64+
}
65+
66+
return {
67+
severity: violations.length > 0 ? 'medium' : 'low',
68+
details: { violations },
69+
};
70+
},
71+
});
72+
73+
function isScannableSourceFile(file: string): boolean {
74+
if (!SCANNABLE_EXT.test(file)) return false;
75+
if (TEST_FILE.test(file)) return false;
76+
if (EXCLUDED_SEGMENT.test(file)) return false;
77+
return true;
78+
}
79+
80+
function extractUsagesFromFiles(
81+
tree: Tree,
82+
files: string[]
83+
): Map<string, Set<string>> {
84+
const usages = new Map<string, Set<string>>();
85+
for (const file of files) {
86+
const content = tree.read(file, 'utf-8');
87+
if (!content) continue;
88+
for (const name of extractUsedVarsFromContent(content, file)) {
89+
let set = usages.get(name);
90+
if (!set) {
91+
set = new Set();
92+
usages.set(name, set);
93+
}
94+
if (set.size < MAX_EXAMPLE_FILES) set.add(file);
95+
}
96+
}
97+
return usages;
98+
}
99+
100+
export function extractDocumentedVars(mdocContent: string): Set<string> {
101+
return new Set(Array.from(mdocContent.matchAll(DOCS_TABLE_ROW), (m) => m[1]));
102+
}
103+
104+
export function extractUsedVarsFromContent(
105+
content: string,
106+
file: string
107+
): string[] {
108+
const patterns = file.endsWith('.rs')
109+
? [RUST_ENV_VAR, RUST_FROM_ENV]
110+
: [TS_JS_USAGE];
111+
const found: string[] = [];
112+
for (const pattern of patterns) {
113+
for (const m of content.matchAll(pattern)) {
114+
found.push(m[1]);
115+
}
116+
}
117+
return found;
118+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"ignore": {
6+
"type": "array",
7+
"items": { "type": "string" },
8+
"description": "NX_* env var names that are intentionally not part of the user-facing documented contract (internal, test-only, or deprecated aliases)."
9+
}
10+
},
11+
"additionalProperties": false
12+
}

0 commit comments

Comments
 (0)