diff --git a/docusaurus/docs/adding-custom-environment-variables.md b/docusaurus/docs/adding-custom-environment-variables.md index 48bfea2cf64..433ee88913c 100644 --- a/docusaurus/docs/adding-custom-environment-variables.md +++ b/docusaurus/docs/adding-custom-environment-variables.md @@ -66,6 +66,38 @@ if (process.env.NODE_ENV !== 'production') { When you compile the app with `npm run build`, the minification step will strip out this condition, and the resulting bundle will be smaller. +## Environment Variable Validation + +> Note: this feature is available with `react-scripts@5.1.0` and higher. + +When you run `npm start` in development mode, Create React App will automatically scan your source code for references to `process.env.REACT_APP_*` variables and check if they are defined in your `.env` files or environment. If you reference an environment variable that isn't defined, you'll see a helpful warning message: + +``` +Warning: The following environment variables are referenced in your code but not defined: + + REACT_APP_API_URL + Did you mean REACT_APP_API_URI? + +To fix this, add the missing variables to your .env file or set them in your environment. +For example, add this line to your .env file: + + REACT_APP_API_URL=your_value_here + +Learn more: https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables + +To disable this check, set DISABLE_ENV_CHECK=true in your environment. +``` + +This validation helps catch common mistakes such as: + +- **Typos in variable names** - The validator will suggest similar variable names if it detects a potential typo +- **Forgotten `.env` entries** - Get immediate feedback when you reference a variable that hasn't been defined yet +- **Case sensitivity issues** - Environment variable names are case-sensitive, and the validator helps identify mismatches + +The validation only runs during development (`npm start`) and won't affect your production builds. Test files (files ending in `.test.js`, `.test.jsx`, `.test.ts`, `.test.tsx`, or files in `__tests__` directories) are excluded from validation since they often use mocked environment variables. + +You can disable this validation by setting `DISABLE_ENV_CHECK=true` in your environment or `.env` file. + ## Referencing Environment Variables in the HTML > Note: this feature is available with `react-scripts@0.9.0` and higher. diff --git a/package-lock.json b/package-lock.json index d9493858556..322db83e4b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29772,7 +29772,7 @@ } }, "packages/babel-plugin-named-asset-import": { - "version": "0.3.8", + "version": "0.4.0", "license": "MIT", "devDependencies": { "babel-plugin-tester": "^10.1.0", @@ -29783,7 +29783,7 @@ } }, "packages/babel-preset-react-app": { - "version": "10.0.1", + "version": "10.1.0", "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", @@ -29813,21 +29813,21 @@ } }, "packages/cra-template": { - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "engines": { "node": ">=14" } }, "packages/cra-template-typescript": { - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "engines": { "node": ">=14" } }, "packages/create-react-app": { - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -29868,7 +29868,7 @@ } }, "packages/eslint-config-react-app": { - "version": "7.0.1", + "version": "7.1.0", "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", @@ -29876,7 +29876,7 @@ "@rushstack/eslint-patch": "^1.1.0", "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", + "babel-preset-react-app": "^10.1.0", "confusing-browser-globals": "^1.0.11", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.25.3", @@ -29909,7 +29909,7 @@ } }, "packages/react-dev-utils": { - "version": "12.0.1", + "version": "12.1.0", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.16.0", @@ -29931,7 +29931,7 @@ "open": "^8.4.0", "pkg-up": "^3.1.0", "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", + "react-error-overlay": "^6.1.0", "recursive-readdir": "^2.2.2", "shell-quote": "^1.7.3", "strip-ansi": "^6.0.1", @@ -29954,7 +29954,7 @@ } }, "packages/react-error-overlay": { - "version": "6.0.11", + "version": "6.1.0", "license": "MIT", "devDependencies": { "@babel/code-frame": "^7.16.0", @@ -29962,7 +29962,7 @@ "anser": "^2.1.0", "babel-jest": "^27.4.2", "babel-loader": "^8.2.3", - "babel-preset-react-app": "^10.0.1", + "babel-preset-react-app": "^10.1.0", "chalk": "^4.1.2", "chokidar": "^3.5.2", "cross-env": "^7.0.3", @@ -29992,7 +29992,7 @@ } }, "packages/react-scripts": { - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", @@ -30000,8 +30000,8 @@ "@svgr/webpack": "^5.5.0", "babel-jest": "^27.4.2", "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", + "babel-plugin-named-asset-import": "^0.4.0", + "babel-preset-react-app": "^10.1.0", "bfj": "^7.0.2", "browserslist": "^4.18.1", "camelcase": "^6.2.1", @@ -30011,7 +30011,7 @@ "dotenv": "^10.0.0", "dotenv-expand": "^5.1.0", "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", + "eslint-config-react-app": "^7.1.0", "eslint-webpack-plugin": "^3.1.1", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", @@ -30028,7 +30028,7 @@ "postcss-preset-env": "^7.0.1", "prompts": "^2.4.2", "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", + "react-dev-utils": "^12.1.0", "react-refresh": "^0.11.0", "resolve": "^1.20.0", "resolve-url-loader": "^4.0.0", @@ -39867,7 +39867,7 @@ "@rushstack/eslint-patch": "^1.1.0", "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", + "babel-preset-react-app": "^10.1.0", "confusing-browser-globals": "^1.0.11", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.25.3", @@ -47318,7 +47318,7 @@ "open": "^8.4.0", "pkg-up": "^3.1.0", "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", + "react-error-overlay": "^6.1.0", "recursive-readdir": "^2.2.2", "shell-quote": "^1.7.3", "strip-ansi": "^6.0.1", @@ -47350,7 +47350,7 @@ "anser": "^2.1.0", "babel-jest": "^27.4.2", "babel-loader": "^8.2.3", - "babel-preset-react-app": "^10.0.1", + "babel-preset-react-app": "^10.1.0", "chalk": "^4.1.2", "chokidar": "^3.5.2", "cross-env": "^7.0.3", @@ -47499,8 +47499,8 @@ "@svgr/webpack": "^5.5.0", "babel-jest": "^27.4.2", "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", + "babel-plugin-named-asset-import": "^0.4.0", + "babel-preset-react-app": "^10.1.0", "bfj": "^7.0.2", "browserslist": "^4.18.1", "camelcase": "^6.2.1", @@ -47510,7 +47510,7 @@ "dotenv": "^10.0.0", "dotenv-expand": "^5.1.0", "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", + "eslint-config-react-app": "^7.1.0", "eslint-webpack-plugin": "^3.1.1", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", @@ -47529,7 +47529,7 @@ "prompts": "^2.4.2", "react": "^19.0.0", "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", + "react-dev-utils": "^12.1.0", "react-dom": "^19.0.0", "react-refresh": "^0.11.0", "resolve": "^1.20.0", diff --git a/packages/create-react-app/given-deprecation-warning b/packages/create-react-app/given-deprecation-warning new file mode 100644 index 00000000000..f32a5804e29 --- /dev/null +++ b/packages/create-react-app/given-deprecation-warning @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/packages/react-dev-utils/README.md b/packages/react-dev-utils/README.md index 4f7185b87a7..a08d6073459 100644 --- a/packages/react-dev-utils/README.md +++ b/packages/react-dev-utils/README.md @@ -131,6 +131,28 @@ if ( } ``` +#### `checkEnvVariables(appSrc: string, isInteractive: boolean): boolean` + +Scans source code for `process.env.REACT_APP_*` references and validates that they are defined.
+Prints helpful warnings for undefined variables, including suggestions for typos.
+Only runs in development mode. Returns `true` to indicate non-blocking validation. + +```js +var path = require('path'); +var checkEnvVariables = require('react-dev-utils/checkEnvVariables'); + +// Check environment variables before starting dev server +checkEnvVariables(path.resolve('src'), true); +``` + +Features: + +- Detects all `REACT_APP_*` environment variable references in `.js`, `.jsx`, `.ts`, and `.tsx` files +- Warns about undefined variables with helpful error messages +- Suggests corrections for potential typos using fuzzy matching +- Excludes test files from validation +- Can be disabled by setting `DISABLE_ENV_CHECK=true` + #### `clearConsole(): void` Clears the console, hopefully in a cross-platform way. diff --git a/packages/react-dev-utils/__tests__/checkEnvVariables.test.js b/packages/react-dev-utils/__tests__/checkEnvVariables.test.js new file mode 100644 index 00000000000..30a82bc963a --- /dev/null +++ b/packages/react-dev-utils/__tests__/checkEnvVariables.test.js @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const checkEnvVariables = require('../checkEnvVariables'); + +describe('checkEnvVariables', () => { + let tempDir; + let originalEnv; + let originalNodeEnv; + let consoleLogSpy; + + beforeEach(() => { + // Create a temporary directory for test files + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-check-test-')); + + // Save original environment + originalEnv = { ...process.env }; + originalNodeEnv = process.env.NODE_ENV; + + // Set to development mode + process.env.NODE_ENV = 'development'; + + // Clear REACT_APP_ variables + Object.keys(process.env).forEach(key => { + if (key.startsWith('REACT_APP_')) { + delete process.env[key]; + } + }); + + // Spy on console.log + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + // Restore environment + process.env = originalEnv; + process.env.NODE_ENV = originalNodeEnv; + + // Restore console.log + consoleLogSpy.mockRestore(); + + // Clean up temp directory + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('should not warn when all referenced variables are defined', () => { + // Create a test file with env variable reference + const testFile = path.join(tempDir, 'App.js'); + fs.writeFileSync(testFile, 'const apiUrl = process.env.REACT_APP_API_URL;'); + + // Define the variable + process.env.REACT_APP_API_URL = 'https://api.example.com'; + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should return true and not log warnings + expect(result).toBe(true); + expect(consoleLogSpy).not.toHaveBeenCalledWith( + expect.stringContaining('not defined') + ); + }); + + test('should warn when referenced variables are undefined', () => { + // Create a test file with env variable reference + const testFile = path.join(tempDir, 'App.js'); + fs.writeFileSync(testFile, 'const apiUrl = process.env.REACT_APP_API_URL;'); + + // Don't define the variable + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should return true but log warnings + expect(result).toBe(true); + expect(consoleLogSpy).toHaveBeenCalled(); + const logOutput = consoleLogSpy.mock.calls.flat().join(' '); + expect(logOutput).toMatch(/REACT_APP_API_URL/); + expect(logOutput).toMatch(/not defined/); + }); + + test('should suggest similar variable names for typos', () => { + // Create a test file with a typo + const testFile = path.join(tempDir, 'App.js'); + fs.writeFileSync( + testFile, + 'const apiUrl = process.env.REACT_APP_API_ULR;' // Typo: ULR instead of URL + ); + + // Define the correct variable + process.env.REACT_APP_API_URL = 'https://api.example.com'; + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should suggest the correct variable name + expect(result).toBe(true); + const logOutput = consoleLogSpy.mock.calls.flat().join(' '); + expect(logOutput).toMatch(/Did you mean.*REACT_APP_API_URL/); + }); + + test('should handle multiple undefined variables', () => { + // Create a test file with multiple undefined variables + const testFile = path.join(tempDir, 'App.js'); + fs.writeFileSync( + testFile, + ` + const apiUrl = process.env.REACT_APP_API_URL; + const apiKey = process.env.REACT_APP_API_KEY; + ` + ); + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should warn about both variables + expect(result).toBe(true); + const logOutput = consoleLogSpy.mock.calls.flat().join(' '); + expect(logOutput).toMatch(/REACT_APP_API_URL/); + expect(logOutput).toMatch(/REACT_APP_API_KEY/); + }); + + test('should skip check when NODE_ENV is not development', () => { + // Set to production mode + process.env.NODE_ENV = 'production'; + + // Create a test file with undefined variable + const testFile = path.join(tempDir, 'App.js'); + fs.writeFileSync(testFile, 'const apiUrl = process.env.REACT_APP_API_URL;'); + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should return true without checking + expect(result).toBe(true); + expect(consoleLogSpy).not.toHaveBeenCalled(); + }); + + test('should skip check when DISABLE_ENV_CHECK is true', () => { + // Disable the check + process.env.DISABLE_ENV_CHECK = 'true'; + + // Create a test file with undefined variable + const testFile = path.join(tempDir, 'App.js'); + fs.writeFileSync(testFile, 'const apiUrl = process.env.REACT_APP_API_URL;'); + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should return true without checking + expect(result).toBe(true); + expect(consoleLogSpy).not.toHaveBeenCalled(); + }); + + test('should handle files in subdirectories', () => { + // Create a subdirectory + const subDir = path.join(tempDir, 'components'); + fs.mkdirSync(subDir); + + // Create a test file in subdirectory + const testFile = path.join(subDir, 'Component.js'); + fs.writeFileSync(testFile, 'const apiUrl = process.env.REACT_APP_API_URL;'); + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should find the variable reference in subdirectory + expect(result).toBe(true); + const logOutput = consoleLogSpy.mock.calls.flat().join(' '); + expect(logOutput).toMatch(/REACT_APP_API_URL/); + }); + + test('should handle TypeScript files', () => { + // Create a TypeScript file + const testFile = path.join(tempDir, 'App.tsx'); + fs.writeFileSync( + testFile, + 'const apiUrl: string = process.env.REACT_APP_API_URL || "";' + ); + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should find the variable reference in TypeScript file + expect(result).toBe(true); + const logOutput = consoleLogSpy.mock.calls.flat().join(' '); + expect(logOutput).toMatch(/REACT_APP_API_URL/); + }); + + test('should ignore test files', () => { + // Create a test file that references an env variable + const testFile = path.join(tempDir, 'App.test.js'); + fs.writeFileSync(testFile, 'const apiUrl = process.env.REACT_APP_API_URL;'); + + // Run the check + const result = checkEnvVariables(tempDir, false); + + // Should not warn about variables in test files + expect(result).toBe(true); + expect(consoleLogSpy).not.toHaveBeenCalledWith( + expect.stringContaining('REACT_APP_API_URL') + ); + }); + + test('should handle errors gracefully', () => { + // Pass a non-existent directory + const result = checkEnvVariables('/non/existent/path', false); + + // Should return true and not throw + expect(result).toBe(true); + }); + + test('should handle empty directory', () => { + // Run check on empty directory + const result = checkEnvVariables(tempDir, false); + + // Should return true without warnings + expect(result).toBe(true); + expect(consoleLogSpy).not.toHaveBeenCalledWith( + expect.stringContaining('not defined') + ); + }); +}); diff --git a/packages/react-dev-utils/checkEnvVariables.js b/packages/react-dev-utils/checkEnvVariables.js new file mode 100644 index 00000000000..33c29b487f2 --- /dev/null +++ b/packages/react-dev-utils/checkEnvVariables.js @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const globby = require('globby'); + +/** + * Calculates the Levenshtein distance between two strings + * Used for fuzzy matching to suggest corrections for typos + */ +function levenshteinDistance(a, b) { + const matrix = []; + + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[b.length][a.length]; +} + +/** + * Finds the closest matching environment variable name + */ +function findClosestMatch(target, candidates) { + let closestMatch = null; + let closestDistance = Infinity; + + for (const candidate of candidates) { + const distance = levenshteinDistance(target, candidate); + if (distance < closestDistance && distance <= 3) { + closestDistance = distance; + closestMatch = candidate; + } + } + + return closestMatch; +} + +/** + * Scans source files for process.env.REACT_APP_* references + */ +function findEnvVariableReferences(appSrc) { + const files = globby.sync(['**/*.{js,jsx,ts,tsx}'], { + cwd: appSrc, + absolute: true, + ignore: ['**/*.test.{js,jsx,ts,tsx}', '**/__tests__/**'], + }); + + const referencedVars = new Set(); + const regex = /process\.env\.(REACT_APP_[A-Z0-9_]+)/g; + + for (const file of files) { + try { + const content = fs.readFileSync(file, 'utf8'); + let match; + while ((match = regex.exec(content)) !== null) { + referencedVars.add(match[1]); + } + } catch (err) { + // Ignore files that can't be read + } + } + + return Array.from(referencedVars); +} + +/** + * Gets all defined REACT_APP_* environment variables + */ +function getDefinedEnvVariables() { + const defined = new Set(); + + for (const key in process.env) { + if (/^REACT_APP_/i.test(key)) { + defined.add(key); + } + } + + return Array.from(defined); +} + +/** + * Checks if referenced environment variables are defined and warns about missing ones + */ +function checkEnvVariables(appSrc, isInteractive = true) { + // Only run in development mode + if (process.env.NODE_ENV !== 'development') { + return true; + } + + // Allow users to opt-out via environment variable + if (process.env.DISABLE_ENV_CHECK === 'true') { + return true; + } + + try { + const referencedVars = findEnvVariableReferences(appSrc); + const definedVars = getDefinedEnvVariables(); + + if (referencedVars.length === 0) { + return true; + } + + const missingVars = referencedVars.filter( + varName => !definedVars.includes(varName) + ); + + if (missingVars.length > 0) { + console.log(); + console.log( + chalk.yellow('Warning: ') + + 'The following environment variables are referenced in your code but not defined:' + ); + console.log(); + + for (const missingVar of missingVars) { + console.log(` ${chalk.cyan(missingVar)}`); + + const suggestion = findClosestMatch(missingVar, definedVars); + if (suggestion) { + console.log( + ` ${chalk.dim('Did you mean')} ${chalk.cyan( + suggestion + )}${chalk.dim('?')}` + ); + } + } + + console.log(); + console.log( + 'To fix this, add the missing variables to your ' + + chalk.cyan('.env') + + ' file or set them in your environment.' + ); + console.log( + 'For example, add this line to your ' + chalk.cyan('.env') + ' file:' + ); + console.log(); + console.log(chalk.dim(' ' + missingVars[0] + '=your_value_here')); + console.log(); + console.log( + 'Learn more: ' + + chalk.cyan( + 'https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables' + ) + ); + console.log(); + console.log( + chalk.dim( + 'To disable this check, set DISABLE_ENV_CHECK=true in your environment.' + ) + ); + console.log(); + } + + return true; + } catch (err) { + // If validation fails for any reason, don't block the build + if (isInteractive) { + console.log(); + console.log( + chalk.yellow( + 'Warning: Unable to validate environment variables. Continuing anyway.' + ) + ); + console.log(); + } + return true; + } +} + +module.exports = checkEnvVariables; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 2cf9d5bb2d3..6e488e074d4 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -17,6 +17,7 @@ "files": [ "browsersHelper.js", "chalk.js", + "checkEnvVariables.js", "checkRequiredFiles.js", "clearConsole.js", "crossSpawn.js", diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 8b9a2c26b4d..7da1345d158 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -28,6 +28,7 @@ const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const clearConsole = require('react-dev-utils/clearConsole'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const checkEnvVariables = require('react-dev-utils/checkEnvVariables'); const { choosePort, createCompiler, @@ -51,6 +52,9 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { process.exit(1); } +// Check for undefined environment variables in development +checkEnvVariables(paths.appSrc, isInteractive); + // Tools like Cloud9 rely on this. const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; const HOST = process.env.HOST || '0.0.0.0';