diff --git a/packages/nx/migrations.json b/packages/nx/migrations.json index cf5fd96773082..f3dddf4f46893 100644 --- a/packages/nx/migrations.json +++ b/packages/nx/migrations.json @@ -83,6 +83,12 @@ "version": "16.6.0-beta.6", "description": "Prefix outputs with {workspaceRoot}/{projectRoot} if needed", "implementation": "./src/migrations/update-15-0-0/prefix-outputs" + }, + "16.8.0-escape-dollar-sign-env": { + "cli": "nx", + "version": "16.8.0-beta.3", + "description": "Escape $ in env variables", + "implementation": "./src/migrations/update-16-8-0/escape-dollar-sign-env-variables" } } } diff --git a/packages/nx/src/migrations/update-16-8-0/escape-dollar-sign-env-variables.spec.ts b/packages/nx/src/migrations/update-16-8-0/escape-dollar-sign-env-variables.spec.ts new file mode 100644 index 0000000000000..dc262102d10a0 --- /dev/null +++ b/packages/nx/src/migrations/update-16-8-0/escape-dollar-sign-env-variables.spec.ts @@ -0,0 +1,132 @@ +import { createTreeWithEmptyWorkspace } from '../../generators/testing-utils/create-tree-with-empty-workspace'; +import { addProjectConfiguration } from '../../generators/utils/project-configuration'; +import escapeDollarSignEnvVariables from './escape-dollar-sign-env-variables'; + +describe('escape $ in env variables', () => { + let tree; + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should escape $ in env variables in .env file', () => { + tree.write( + '.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + escapeDollarSignEnvVariables(tree); + expect(tree.read('.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + }); + + it('should escape $ env variables in .env file under project', () => { + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + }); + addProjectConfiguration(tree, 'my-app2', { + root: 'apps/my-app2', + }); + tree.write( + 'apps/my-app/.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + tree.write( + 'apps/my-app2/.env', + `dollar=$ +NX_SOME_VAR=$DEF` + ); + escapeDollarSignEnvVariables(tree); + expect(tree.read('apps/my-app/.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + expect(tree.read('apps/my-app2/.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$DEF`); + }); + + it('should escape $ env variables in .env for target', () => { + tree.write('.env', 'dollar=$'); + tree.write('.env.build', 'dollar=$'); + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + targets: { + build: { + executor: '@nx/node:build', + configurations: { + production: {}, + }, + }, + }, + }); + tree.write( + 'apps/my-app/.build.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + tree.write( + 'apps/my-app/.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + escapeDollarSignEnvVariables(tree); + expect(tree.read('.env', 'utf-8')).toEqual(`dollar=\\$`); + expect(tree.read('apps/my-app/.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + expect(tree.read('apps/my-app/.build.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + }); + + it('should escape $ env variables in .env for configuration', () => { + tree.write('.env', 'dollar=$'); + tree.write('.env.production', 'dollar=$'); + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + targets: { + build: { + executor: '@nx/node:build', + configurations: { + production: {}, + }, + }, + }, + }); + tree.write( + 'apps/my-app/.production.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + tree.write( + 'apps/my-app/.build.production.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + tree.write( + 'apps/my-app/.env', + `dollar=$ +NX_SOME_VAR=$ABC` + ); + escapeDollarSignEnvVariables(tree); + expect(tree.read('.env', 'utf-8')).toEqual(`dollar=\\$`); + expect(tree.read('apps/my-app/.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + expect(tree.read('apps/my-app/.build.production.env', 'utf-8')) + .toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + expect(tree.read('apps/my-app/.production.env', 'utf-8')) + .toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + }); + + it('should not escape $ env variables if it is already escaped', () => { + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + }); + tree.write( + 'apps/my-app/.env', + `dollar=\\$ +NX_SOME_VAR=\\$ABC` + ); + escapeDollarSignEnvVariables(tree); + expect(tree.read('apps/my-app/.env', 'utf-8')).toEqual(`dollar=\\$ +NX_SOME_VAR=\\$ABC`); + }); +}); diff --git a/packages/nx/src/migrations/update-16-8-0/escape-dollar-sign-env-variables.ts b/packages/nx/src/migrations/update-16-8-0/escape-dollar-sign-env-variables.ts new file mode 100644 index 0000000000000..de32897c0eb42 --- /dev/null +++ b/packages/nx/src/migrations/update-16-8-0/escape-dollar-sign-env-variables.ts @@ -0,0 +1,76 @@ +import { Tree } from '../../generators/tree'; +import { getProjects } from '../../generators/utils/project-configuration'; + +/** + * This function escapes dollar sign in env variables + * It will go through: + * - '.env', '.local.env', '.env.local' + * - .env.[target-name], .[target-name].env + * - .env.[target-name].[configuration-name], .[target-name].[configuration-name].env + * - .env.[configuration-name], .[configuration-name].env + * at each project root and workspace root + * @param tree + */ +export default function escapeDollarSignEnvVariables(tree: Tree) { + const envFiles = ['.env', '.local.env', '.env.local']; + for (const [_, configuration] of getProjects(tree).entries()) { + envFiles.push( + `${configuration.root}/.env`, + `${configuration.root}/.local.env`, + `${configuration.root}/.env.local` + ); + for (const targetName in configuration.targets) { + const task = configuration.targets[targetName]; + envFiles.push( + `.env.${targetName}`, + `.${targetName}.env`, + `${configuration.root}/.env.${targetName}`, + `${configuration.root}/.${targetName}.env` + ); + + if (task.configurations) { + for (const configurationName in task.configurations) { + envFiles.push( + `.env.${targetName}.${configurationName}`, + `.${targetName}.${configurationName}.env`, + `.env.${configurationName}`, + `.${configurationName}.env`, + `${configuration.root}/.env.${targetName}.${configurationName}`, + `${configuration.root}/.${targetName}.${configurationName}.env`, + `${configuration.root}/.env.${configurationName}`, + `${configuration.root}/.${configurationName}.env` + ); + } + } + } + } + for (const envFile of new Set(envFiles)) { + parseEnvFile(tree, envFile); + } +} + +/** + * This function parse the env file and escape dollar sign + * @param tree + * @param envFilePath + * @returns + */ +function parseEnvFile(tree: Tree, envFilePath: string) { + if (!tree.exists(envFilePath)) { + return; + } + let envFileContent = tree.read(envFilePath, 'utf-8'); + envFileContent = envFileContent + .split('\n') + .map((line) => { + line = line.trim(); + const declarations = line.split('='); + if (declarations[1].includes('$') && !declarations[1].includes(`\\$`)) { + declarations[1] = declarations[1].replace('$', `\\$`); + line = declarations.join('='); + } + return line; + }) + .join('\n'); + tree.write(envFilePath, envFileContent); +}