Skip to content

Commit ef3d1e2

Browse files
xiongemimatheo
authored andcommitted
fix(core): expand env variables on load and unload (#26459)
This pr is meant to replace #22585 and #20524 Env variables using other variables were not unloaded from the environment and further customizations were impossible in more specific env files. <!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> ## Current Behavior <!-- This is the behavior we have today --> ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #23581 Co-authored-by: Mateo Tibaquira <[email protected]> (cherry picked from commit 88fd03b)
1 parent 3abd352 commit ef3d1e2

File tree

7 files changed

+182
-104
lines changed

7 files changed

+182
-104
lines changed

e2e/nx/src/extras.test.ts

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { parseJson } from '@nx/devkit';
22
import {
33
checkFilesExist,
44
cleanupProject,
5-
getSelectedPackageManager,
65
isNotWindows,
76
newProject,
87
readFile,
@@ -293,49 +292,64 @@ describe('Extra Nx Misc Tests', () => {
293292
});
294293

295294
describe('Env File', () => {
296-
it('should have the right env', () => {
297-
const appName = uniq('app');
295+
const libName = uniq('lib');
296+
297+
beforeAll(() => {
298298
runCLI(
299-
`generate @nx/react:app ${appName} --style=css --bundler=webpack --no-interactive`
299+
`generate @nx/js:lib ${libName} --bundler=none --unitTestRunner=none --no-interactive`
300300
);
301+
});
302+
303+
it('should have the right env', () => {
301304
updateFile(
302305
'.env',
303306
`FIRSTNAME="firstname"
304-
LASTNAME="lastname"
305-
NX_USERNAME=$FIRSTNAME $LASTNAME`
306-
);
307-
updateFile(
308-
`apps/${appName}/src/app/app.tsx`,
309-
`
310-
import NxWelcome from './nx-welcome';
311-
312-
export function App() {
313-
return (
314-
<>
315-
<NxWelcome title={process.env.NX_USERNAME} />
316-
</>
317-
);
318-
}
319-
320-
export default App;
321-
`
307+
LASTNAME="lastname"
308+
NX_USERNAME=$FIRSTNAME $LASTNAME`
322309
);
323-
updateFile(
324-
`apps/${appName}/src/app/app.spec.tsx`,
325-
`import { render } from '@testing-library/react';
326-
327-
import App from './app';
328-
329-
describe('App', () => {
330-
it('should have a greeting as the title', () => {
331-
const { getByText } = render(<App />);
332-
expect(getByText(/Welcome firstname lastname/gi)).toBeTruthy();
310+
updateJson(join('libs', libName, 'project.json'), (config) => {
311+
config.targets.echo = {
312+
command: 'echo $NX_USERNAME',
313+
};
314+
return config;
315+
});
316+
let result = runCLI(`run ${libName}:echo`);
317+
expect(result).toContain('firstname lastname');
318+
319+
updateFile('.env', (content) => {
320+
content = content.replace('firstname', 'firstname2');
321+
content = content.replace('lastname', 'lastname2');
322+
return content;
333323
});
324+
result = runCLI(`run ${libName}:echo`);
325+
expect(result).toContain('firstname2 lastname2');
334326
});
335-
`
336-
);
337-
const unitTestsOutput = runCLI(`test ${appName}`);
338-
expect(unitTestsOutput).toContain('Successfully ran target test');
327+
328+
it('should work with custom env file', () => {
329+
updateFile(`libs/${libName}/.custom1.env`, `hello="hello1"`);
330+
updateFile(`libs/${libName}/.custom2.env`, `hello="hello2"`);
331+
updateJson(join('libs', libName, 'project.json'), (config) => {
332+
config.targets.hello1 = {
333+
command: 'echo $hello',
334+
options: {
335+
envFile: `libs/${libName}/.custom1.env`,
336+
},
337+
};
338+
config.targets.hello2 = {
339+
command: 'echo $hello',
340+
options: {
341+
envFile: `libs/${libName}/.custom2.env`,
342+
},
343+
};
344+
return config;
345+
});
346+
let result = runCLI(`run ${libName}:hello1`);
347+
expect(result).toContain('hello1');
348+
result = runCLI(`run ${libName}:hello2`);
349+
expect(result).toContain('hello2');
350+
result = runCLI(`run-many --target=hello1,hello2`);
351+
expect(result).toContain('hello1');
352+
expect(result).toContain('hello2');
339353
});
340354
});
341355

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@
160160
"cz-git": "^1.4.0",
161161
"czg": "^1.4.0",
162162
"detect-port": "^1.5.1",
163-
"dotenv": "~16.3.1",
164-
"dotenv-expand": "^10.0.0",
163+
"dotenv": "~16.4.5",
164+
"dotenv-expand": "~11.0.6",
165165
"ejs": "^3.1.7",
166166
"enhanced-resolve": "^5.8.3",
167167
"esbuild": "0.19.5",

packages/nx/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
"cli-cursor": "3.1.0",
4646
"cli-spinners": "2.6.1",
4747
"cliui": "^8.0.1",
48-
"dotenv": "~16.3.1",
49-
"dotenv-expand": "~10.0.0",
48+
"dotenv": "~16.4.5",
49+
"dotenv-expand": "~11.0.6",
5050
"enquirer": "~2.3.6",
5151
"figures": "3.2.0",
5252
"flat": "^5.0.2",

packages/nx/src/executors/run-commands/run-commands.impl.spec.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFileSync, unlinkSync, writeFileSync } from 'fs';
1+
import { appendFileSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
22
import { relative } from 'path';
33
import { dirSync, fileSync } from 'tmp';
44
import runCommands, {
@@ -337,8 +337,6 @@ describe('Run Commands', () => {
337337
result = res;
338338
});
339339

340-
expect(readFile(f)).toEqual('');
341-
342340
setTimeout(() => {
343341
expect(readFile(f)).toEqual('1');
344342
expect(result).toBeNull();
@@ -808,7 +806,7 @@ describe('Run Commands', () => {
808806
});
809807

810808
it('should load the root .env file by default if there is one', async () => {
811-
let f = fileSync().name;
809+
const f = fileSync().name;
812810
const result = await runCommands(
813811
{
814812
commands: [
@@ -828,8 +826,8 @@ describe('Run Commands', () => {
828826
it('should load the specified .env file instead of the root one', async () => {
829827
const devEnv = fileSync().name;
830828
writeFileSync(devEnv, 'NX_SITE=https://nx.dev/');
831-
let f = fileSync().name;
832-
const result = await runCommands(
829+
const f = fileSync().name;
830+
let result = await runCommands(
833831
{
834832
commands: [
835833
{
@@ -843,11 +841,27 @@ describe('Run Commands', () => {
843841
);
844842

845843
expect(result).toEqual(expect.objectContaining({ success: true }));
846-
expect(readFile(f)).toEqual('https://nx.dev/');
844+
expect(readFile(f)).toContain('https://nx.dev/');
845+
846+
appendFileSync(devEnv, 'NX_TEST=$NX_SITE');
847+
await runCommands(
848+
{
849+
commands: [
850+
{
851+
command: `echo $NX_TEST >> ${f}`,
852+
},
853+
],
854+
envFile: devEnv,
855+
__unparsed__: [],
856+
},
857+
context
858+
);
859+
expect(result).toEqual(expect.objectContaining({ success: true }));
860+
expect(readFile(f)).toContain('https://nx.dev/');
847861
});
848862

849863
it('should error if the specified .env file does not exist', async () => {
850-
let f = fileSync().name;
864+
const f = fileSync().name;
851865
try {
852866
await runCommands(
853867
{

packages/nx/src/executors/run-commands/run-commands.impl.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,29 @@ import {
1010
PseudoTtyProcess,
1111
} from '../../tasks-runner/pseudo-terminal';
1212
import { signalToCode } from '../../utils/exit-codes';
13+
import {
14+
loadAndExpandDotEnvFile,
15+
unloadDotEnvFile,
16+
} from '../../tasks-runner/task-env';
1317

1418
export const LARGE_BUFFER = 1024 * 1000000;
1519
let pseudoTerminal: PseudoTerminal | null;
1620
const childProcesses = new Set<ChildProcess | PseudoTtyProcess>();
1721

18-
async function loadEnvVars(path?: string) {
22+
function loadEnvVarsFile(path: string, env: Record<string, string> = {}) {
23+
unloadDotEnvFile(path, env);
24+
const result = loadAndExpandDotEnvFile(path, env);
25+
if (result.error) {
26+
throw result.error;
27+
}
28+
}
29+
30+
function loadEnvVars(path?: string, env: Record<string, string> = {}) {
1931
if (path) {
20-
const result = (await import('dotenv')).config({ path });
21-
if (result.error) {
22-
throw result.error;
23-
}
32+
loadEnvVarsFile(path, env);
2433
} else {
2534
try {
26-
(await import('dotenv')).config();
35+
loadEnvVarsFile('.env', env);
2736
} catch {}
2837
}
2938
}
@@ -109,9 +118,6 @@ export default async function (
109118
terminalOutput: string;
110119
}> {
111120
registerProcessListener();
112-
if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false') {
113-
await loadEnvVars(options.envFile);
114-
}
115121
const normalized = normalizeOptions(options);
116122

117123
if (normalized.readyWhenStatus.length && !normalized.parallel) {
@@ -159,7 +165,8 @@ async function runInParallel(
159165
true,
160166
options.usePty,
161167
options.streamOutput,
162-
options.tty
168+
options.tty,
169+
options.envFile
163170
).then((result: { success: boolean; terminalOutput: string }) => ({
164171
result,
165172
command: c.command,
@@ -287,11 +294,12 @@ async function runSerially(
287294
[],
288295
options.color,
289296
calculateCwd(options.cwd, context),
290-
options.env ?? {},
297+
options.processEnv ?? options.env ?? {},
291298
false,
292299
options.usePty,
293300
options.streamOutput,
294-
options.tty
301+
options.tty,
302+
options.envFile
295303
);
296304
terminalOutput += result.terminalOutput;
297305
if (!result.success) {
@@ -321,9 +329,10 @@ async function createProcess(
321329
isParallel: boolean,
322330
usePty: boolean = true,
323331
streamOutput: boolean = true,
324-
tty: boolean
332+
tty: boolean,
333+
envFile?: string
325334
): Promise<{ success: boolean; terminalOutput: string }> {
326-
env = processEnv(color, cwd, env);
335+
env = processEnv(color, cwd, env, envFile);
327336
// The rust runCommand is always a tty, so it will not look nice in parallel and if we need prefixes
328337
// currently does not work properly in windows
329338
if (
@@ -462,13 +471,21 @@ function calculateCwd(
462471
return path.join(context.root, cwd);
463472
}
464473

465-
function processEnv(color: boolean, cwd: string, env: Record<string, string>) {
474+
function processEnv(
475+
color: boolean,
476+
cwd: string,
477+
env: Record<string, string>,
478+
envFile?: string
479+
) {
466480
const localEnv = appendLocalEnv({ cwd: cwd ?? process.cwd() });
467481
const res = {
468482
...process.env,
469483
...localEnv,
470484
...env,
471485
};
486+
if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false') {
487+
loadEnvVars(envFile, res);
488+
}
472489
// need to override PATH to make sure we are using the local node_modules
473490
if (localEnv.PATH) res.PATH = localEnv.PATH; // UNIX-like
474491
if (localEnv.Path) res.Path = localEnv.Path; // Windows

0 commit comments

Comments
 (0)