diff --git a/e2e/nx/src/extras.test.ts b/e2e/nx/src/extras.test.ts
index 71aec7f6dce9b..2b9a1ad445cff 100644
--- a/e2e/nx/src/extras.test.ts
+++ b/e2e/nx/src/extras.test.ts
@@ -2,7 +2,6 @@ import { parseJson } from '@nx/devkit';
import {
checkFilesExist,
cleanupProject,
- getSelectedPackageManager,
isNotWindows,
newProject,
readFile,
@@ -293,49 +292,64 @@ describe('Extra Nx Misc Tests', () => {
});
describe('Env File', () => {
- it('should have the right env', () => {
- const appName = uniq('app');
+ const libName = uniq('lib');
+
+ beforeAll(() => {
runCLI(
- `generate @nx/react:app ${appName} --style=css --bundler=webpack --no-interactive`
+ `generate @nx/js:lib ${libName} --bundler=none --unitTestRunner=none --no-interactive`
);
+ });
+
+ it('should have the right env', () => {
updateFile(
'.env',
`FIRSTNAME="firstname"
- LASTNAME="lastname"
- NX_USERNAME=$FIRSTNAME $LASTNAME`
- );
- updateFile(
- `apps/${appName}/src/app/app.tsx`,
- `
- import NxWelcome from './nx-welcome';
-
- export function App() {
- return (
- <>
-
- >
- );
- }
-
- export default App;
- `
+LASTNAME="lastname"
+NX_USERNAME=$FIRSTNAME $LASTNAME`
);
- updateFile(
- `apps/${appName}/src/app/app.spec.tsx`,
- `import { render } from '@testing-library/react';
-
- import App from './app';
-
- describe('App', () => {
- it('should have a greeting as the title', () => {
- const { getByText } = render();
- expect(getByText(/Welcome firstname lastname/gi)).toBeTruthy();
+ updateJson(join('libs', libName, 'project.json'), (config) => {
+ config.targets.echo = {
+ command: 'echo $NX_USERNAME',
+ };
+ return config;
+ });
+ let result = runCLI(`run ${libName}:echo`);
+ expect(result).toContain('firstname lastname');
+
+ updateFile('.env', (content) => {
+ content = content.replace('firstname', 'firstname2');
+ content = content.replace('lastname', 'lastname2');
+ return content;
});
+ result = runCLI(`run ${libName}:echo`);
+ expect(result).toContain('firstname2 lastname2');
});
- `
- );
- const unitTestsOutput = runCLI(`test ${appName}`);
- expect(unitTestsOutput).toContain('Successfully ran target test');
+
+ it('should work with custom env file', () => {
+ updateFile(`libs/${libName}/.custom1.env`, `hello="hello1"`);
+ updateFile(`libs/${libName}/.custom2.env`, `hello="hello2"`);
+ updateJson(join('libs', libName, 'project.json'), (config) => {
+ config.targets.hello1 = {
+ command: 'echo $hello',
+ options: {
+ envFile: `libs/${libName}/.custom1.env`,
+ },
+ };
+ config.targets.hello2 = {
+ command: 'echo $hello',
+ options: {
+ envFile: `libs/${libName}/.custom2.env`,
+ },
+ };
+ return config;
+ });
+ let result = runCLI(`run ${libName}:hello1`);
+ expect(result).toContain('hello1');
+ result = runCLI(`run ${libName}:hello2`);
+ expect(result).toContain('hello2');
+ result = runCLI(`run-many --target=hello1,hello2`);
+ expect(result).toContain('hello1');
+ expect(result).toContain('hello2');
});
});
diff --git a/package.json b/package.json
index 10200d4e7cb2f..a2bcba834d335 100644
--- a/package.json
+++ b/package.json
@@ -160,8 +160,8 @@
"cz-git": "^1.4.0",
"czg": "^1.4.0",
"detect-port": "^1.5.1",
- "dotenv": "~16.3.1",
- "dotenv-expand": "^10.0.0",
+ "dotenv": "~16.4.5",
+ "dotenv-expand": "~11.0.6",
"ejs": "^3.1.7",
"enhanced-resolve": "^5.8.3",
"esbuild": "0.19.5",
diff --git a/packages/nx/package.json b/packages/nx/package.json
index c6d0f179af7d7..578bd7b9d88f3 100644
--- a/packages/nx/package.json
+++ b/packages/nx/package.json
@@ -45,8 +45,8 @@
"cli-cursor": "3.1.0",
"cli-spinners": "2.6.1",
"cliui": "^8.0.1",
- "dotenv": "~16.3.1",
- "dotenv-expand": "~10.0.0",
+ "dotenv": "~16.4.5",
+ "dotenv-expand": "~11.0.6",
"enquirer": "~2.3.6",
"figures": "3.2.0",
"flat": "^5.0.2",
diff --git a/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts b/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts
index 0222bfc02d369..a7bb1b796de8d 100644
--- a/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts
+++ b/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts
@@ -1,4 +1,4 @@
-import { readFileSync, unlinkSync, writeFileSync } from 'fs';
+import { appendFileSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
import { relative } from 'path';
import { dirSync, fileSync } from 'tmp';
import runCommands, {
@@ -337,8 +337,6 @@ describe('Run Commands', () => {
result = res;
});
- expect(readFile(f)).toEqual('');
-
setTimeout(() => {
expect(readFile(f)).toEqual('1');
expect(result).toBeNull();
@@ -808,7 +806,7 @@ describe('Run Commands', () => {
});
it('should load the root .env file by default if there is one', async () => {
- let f = fileSync().name;
+ const f = fileSync().name;
const result = await runCommands(
{
commands: [
@@ -828,8 +826,8 @@ describe('Run Commands', () => {
it('should load the specified .env file instead of the root one', async () => {
const devEnv = fileSync().name;
writeFileSync(devEnv, 'NX_SITE=https://nx.dev/');
- let f = fileSync().name;
- const result = await runCommands(
+ const f = fileSync().name;
+ let result = await runCommands(
{
commands: [
{
@@ -843,11 +841,27 @@ describe('Run Commands', () => {
);
expect(result).toEqual(expect.objectContaining({ success: true }));
- expect(readFile(f)).toEqual('https://nx.dev/');
+ expect(readFile(f)).toContain('https://nx.dev/');
+
+ appendFileSync(devEnv, 'NX_TEST=$NX_SITE');
+ await runCommands(
+ {
+ commands: [
+ {
+ command: `echo $NX_TEST >> ${f}`,
+ },
+ ],
+ envFile: devEnv,
+ __unparsed__: [],
+ },
+ context
+ );
+ expect(result).toEqual(expect.objectContaining({ success: true }));
+ expect(readFile(f)).toContain('https://nx.dev/');
});
it('should error if the specified .env file does not exist', async () => {
- let f = fileSync().name;
+ const f = fileSync().name;
try {
await runCommands(
{
diff --git a/packages/nx/src/executors/run-commands/run-commands.impl.ts b/packages/nx/src/executors/run-commands/run-commands.impl.ts
index f0543b2cb5ec0..20d327f10ca4c 100644
--- a/packages/nx/src/executors/run-commands/run-commands.impl.ts
+++ b/packages/nx/src/executors/run-commands/run-commands.impl.ts
@@ -10,20 +10,29 @@ import {
PseudoTtyProcess,
} from '../../tasks-runner/pseudo-terminal';
import { signalToCode } from '../../utils/exit-codes';
+import {
+ loadAndExpandDotEnvFile,
+ unloadDotEnvFile,
+} from '../../tasks-runner/task-env';
export const LARGE_BUFFER = 1024 * 1000000;
let pseudoTerminal: PseudoTerminal | null;
const childProcesses = new Set();
-async function loadEnvVars(path?: string) {
+function loadEnvVarsFile(path: string, env: Record = {}) {
+ unloadDotEnvFile(path, env);
+ const result = loadAndExpandDotEnvFile(path, env);
+ if (result.error) {
+ throw result.error;
+ }
+}
+
+function loadEnvVars(path?: string, env: Record = {}) {
if (path) {
- const result = (await import('dotenv')).config({ path });
- if (result.error) {
- throw result.error;
- }
+ loadEnvVarsFile(path, env);
} else {
try {
- (await import('dotenv')).config();
+ loadEnvVarsFile('.env', env);
} catch {}
}
}
@@ -109,9 +118,6 @@ export default async function (
terminalOutput: string;
}> {
registerProcessListener();
- if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false') {
- await loadEnvVars(options.envFile);
- }
const normalized = normalizeOptions(options);
if (normalized.readyWhenStatus.length && !normalized.parallel) {
@@ -159,7 +165,8 @@ async function runInParallel(
true,
options.usePty,
options.streamOutput,
- options.tty
+ options.tty,
+ options.envFile
).then((result: { success: boolean; terminalOutput: string }) => ({
result,
command: c.command,
@@ -287,11 +294,12 @@ async function runSerially(
[],
options.color,
calculateCwd(options.cwd, context),
- options.env ?? {},
+ options.processEnv ?? options.env ?? {},
false,
options.usePty,
options.streamOutput,
- options.tty
+ options.tty,
+ options.envFile
);
terminalOutput += result.terminalOutput;
if (!result.success) {
@@ -321,9 +329,10 @@ async function createProcess(
isParallel: boolean,
usePty: boolean = true,
streamOutput: boolean = true,
- tty: boolean
+ tty: boolean,
+ envFile?: string
): Promise<{ success: boolean; terminalOutput: string }> {
- env = processEnv(color, cwd, env);
+ env = processEnv(color, cwd, env, envFile);
// The rust runCommand is always a tty, so it will not look nice in parallel and if we need prefixes
// currently does not work properly in windows
if (
@@ -462,13 +471,21 @@ function calculateCwd(
return path.join(context.root, cwd);
}
-function processEnv(color: boolean, cwd: string, env: Record) {
+function processEnv(
+ color: boolean,
+ cwd: string,
+ env: Record,
+ envFile?: string
+) {
const localEnv = appendLocalEnv({ cwd: cwd ?? process.cwd() });
const res = {
...process.env,
...localEnv,
...env,
};
+ if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false') {
+ loadEnvVars(envFile, res);
+ }
// need to override PATH to make sure we are using the local node_modules
if (localEnv.PATH) res.PATH = localEnv.PATH; // UNIX-like
if (localEnv.Path) res.Path = localEnv.Path; // Windows
diff --git a/packages/nx/src/tasks-runner/task-env.ts b/packages/nx/src/tasks-runner/task-env.ts
index 2c971a5936246..67d85afac0197 100644
--- a/packages/nx/src/tasks-runner/task-env.ts
+++ b/packages/nx/src/tasks-runner/task-env.ts
@@ -121,12 +121,50 @@ function getNxEnvVariablesForTask(
};
}
-function loadDotEnvFilesForTask(
- task: Task,
- environmentVariables: NodeJS.ProcessEnv
+/**
+ * This function loads a .env file and expands the variables in it.
+ * It is going to override existing environmentVariables.
+ * @param filename
+ * @param environmentVariables
+ */
+export function loadAndExpandDotEnvFile(
+ filename: string,
+ environmentVariables: NodeJS.ProcessEnv,
+ override = false
) {
+ const myEnv = loadDotEnvFile({
+ path: filename,
+ processEnv: environmentVariables,
+ override,
+ });
+ return expand({
+ ...myEnv,
+ processEnv: environmentVariables,
+ });
+}
+
+/**
+ * This function unloads a .env file and removes the variables in it from the environmentVariables.
+ * @param filename
+ * @param environmentVariables
+ */
+export function unloadDotEnvFile(
+ filename: string,
+ environmentVariables: NodeJS.ProcessEnv,
+ override = false
+) {
+ const parsedDotEnvFile: NodeJS.ProcessEnv = {};
+ loadAndExpandDotEnvFile(filename, parsedDotEnvFile, override);
+ Object.keys(parsedDotEnvFile).forEach((envVarKey) => {
+ if (environmentVariables[envVarKey] === parsedDotEnvFile[envVarKey]) {
+ delete environmentVariables[envVarKey];
+ }
+ });
+}
+
+function getEnvFilesForTask(task: Task): string[] {
// Collect dot env files that may pertain to a task
- const dotEnvFiles = [
+ return [
// Load DotEnv Files for a configuration in the project root
...(task.target.configuration
? [
@@ -175,39 +213,22 @@ function loadDotEnvFilesForTask(
`.env.local`,
`.env`,
];
+}
+function loadDotEnvFilesForTask(
+ task: Task,
+ environmentVariables: NodeJS.ProcessEnv
+) {
+ const dotEnvFiles = getEnvFilesForTask(task);
for (const file of dotEnvFiles) {
- const myEnv = loadDotEnvFile({
- path: file,
- processEnv: environmentVariables,
- // Do not override existing env variables as we load
- override: false,
- });
- environmentVariables = {
- ...expand({
- ...myEnv,
- ignoreProcessEnv: true, // Do not override existing env variables as we load
- }).parsed,
- ...environmentVariables,
- };
+ loadAndExpandDotEnvFile(file, environmentVariables);
}
-
return environmentVariables;
}
function unloadDotEnvFiles(environmentVariables: NodeJS.ProcessEnv) {
- const unloadDotEnvFile = (filename: string) => {
- let parsedDotEnvFile: NodeJS.ProcessEnv = {};
- loadDotEnvFile({ path: filename, processEnv: parsedDotEnvFile });
- Object.keys(parsedDotEnvFile).forEach((envVarKey) => {
- if (environmentVariables[envVarKey] === parsedDotEnvFile[envVarKey]) {
- delete environmentVariables[envVarKey];
- }
- });
- };
-
for (const file of ['.env', '.local.env', '.env.local']) {
- unloadDotEnvFile(file);
+ unloadDotEnvFile(file, environmentVariables);
}
return environmentVariables;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c62ab79ec01a6..d510637825b13 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -555,11 +555,11 @@ devDependencies:
specifier: ^1.5.1
version: 1.5.1
dotenv:
- specifier: ~16.3.1
- version: 16.3.1
+ specifier: ~16.4.5
+ version: 16.4.5
dotenv-expand:
- specifier: ^10.0.0
- version: 10.0.0
+ specifier: ~11.0.6
+ version: 11.0.6
ejs:
specifier: ^3.1.7
version: 3.1.8
@@ -9874,7 +9874,7 @@ packages:
create-require: 1.1.1
defu: 6.1.4
destr: 2.0.2
- dotenv: 16.3.1
+ dotenv: 16.4.5
git-url-parse: 13.1.1
is-docker: 3.0.0
jiti: 1.21.0
@@ -11981,7 +11981,7 @@ packages:
chalk: 4.1.2
chokidar: 3.5.3
cross-spawn: 7.0.3
- dotenv: 16.3.1
+ dotenv: 16.4.5
es-module-lexer: 1.4.1
esbuild: 0.17.6
esbuild-plugins-node-modules-polyfill: 1.6.1(esbuild@0.17.6)
@@ -17903,7 +17903,7 @@ packages:
dependencies:
chokidar: 3.5.3
defu: 6.1.4
- dotenv: 16.3.1
+ dotenv: 16.4.5
giget: 1.2.1
jiti: 1.21.0
mlly: 1.5.0
@@ -20090,8 +20090,20 @@ packages:
engines: {node: '>=12'}
dev: true
- /dotenv@16.3.1:
- resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
+ /dotenv-expand@11.0.6:
+ resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==}
+ engines: {node: '>=12'}
+ dependencies:
+ dotenv: 16.4.5
+ dev: true
+
+ /dotenv@16.3.2:
+ resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /dotenv@16.4.5:
+ resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'}
dev: true
@@ -25118,7 +25130,7 @@ packages:
engines: {node: '>=14.0.0'}
dependencies:
app-root-dir: 1.0.2
- dotenv: 16.3.1
+ dotenv: 16.4.5
dotenv-expand: 10.0.0
dev: true
@@ -27704,7 +27716,7 @@ packages:
cli-cursor: 3.1.0
cli-spinners: 2.6.1
cliui: 8.0.1
- dotenv: 16.3.1
+ dotenv: 16.3.2
dotenv-expand: 10.0.0
enquirer: 2.3.6
figures: 3.2.0
@@ -27769,7 +27781,7 @@ packages:
cli-cursor: 3.1.0
cli-spinners: 2.6.1
cliui: 8.0.1
- dotenv: 16.3.1
+ dotenv: 16.3.2
dotenv-expand: 10.0.0
enquirer: 2.3.6
figures: 3.2.0