|
| 1 | +import { confirm } from '@inquirer/prompts'; |
| 2 | +import fs from 'fs-extra'; |
| 3 | +import ora from 'ora'; |
| 4 | +import path from 'path'; |
| 5 | +import { globals } from '../constants'; |
| 6 | +import addDependency from '../util/addDependency'; |
| 7 | +import addToGitignore from '../util/addToGitignore'; |
| 8 | +import commit from '../util/commit'; |
| 9 | +import copyTemplate from '../util/copyTemplate'; |
| 10 | +import getProjectDir from '../util/getProjectDir'; |
| 11 | +import print from '../util/print'; |
| 12 | +import writeFile from '../util/writeFile'; |
| 13 | + |
| 14 | +type Options = { |
| 15 | + interactive?: boolean; |
| 16 | +}; |
| 17 | + |
| 18 | +const handleCommitError = (error: { stdout: string }) => { |
| 19 | + if (!error.stdout.includes('nothing to commit')) { |
| 20 | + throw error; |
| 21 | + } |
| 22 | +}; |
| 23 | + |
| 24 | +const API_PATH = 'src/util/api/api.ts'; |
| 25 | +const HARDCODED_API_URL = "'https://api.github.com/orgs/thoughtbot/repos'"; |
| 26 | +// eslint-disable-next-line no-template-curly-in-string |
| 27 | +const REPLACED_API_URL = "`${process.env.EXPO_PUBLIC_API_BASE_URL ?? ''}/orgs/thoughtbot/repos`"; |
| 28 | + |
| 29 | +const JEST_CONFIG_PATH = 'jest.config.js'; |
| 30 | +const JEST_SETUP_FILES_BEFORE = ' setupFilesAfterEnv: ['; |
| 31 | +const JEST_SETUP_FILES_AFTER = |
| 32 | + " setupFiles: ['./jest.setup.env.js'],\n setupFilesAfterEnv: ["; |
| 33 | + |
| 34 | +export async function addEnv(options: Options = {}) { |
| 35 | + const { interactive = true } = options; |
| 36 | + |
| 37 | + globals.interactive = interactive; |
| 38 | + |
| 39 | + await printIntro(); |
| 40 | + |
| 41 | + const spinner = ora().start('Setting up environment configuration'); |
| 42 | + |
| 43 | + const projectDir = await getProjectDir(); |
| 44 | + |
| 45 | + await addDependency('dotenv', { dev: true }); |
| 46 | + |
| 47 | + await copyTemplate({ templateDir: 'environments', templateFile: 'env.example', destination: '.env.example' }); |
| 48 | + await copyTemplate({ templateDir: 'environments', templateFile: 'src/config/index.ts' }); |
| 49 | + await copyTemplate({ templateDir: 'environments', templateFile: 'jest.setup.env.js' }); |
| 50 | + await copyTemplate({ templateDir: 'environments', templateFile: 'env.test', destination: '.env.test' }); |
| 51 | + |
| 52 | + const envPath = path.join(projectDir, '.env'); |
| 53 | + const envExists = await fs.pathExists(envPath); |
| 54 | + if (!envExists) { |
| 55 | + await fs.copy(path.join(projectDir, '.env.example'), envPath); |
| 56 | + } |
| 57 | + |
| 58 | + await addToGitignore('.env'); |
| 59 | + |
| 60 | + const apiFilePath = path.join(projectDir, API_PATH); |
| 61 | + const apiFileExists = await fs.pathExists(apiFilePath); |
| 62 | + let patchedApi = false; |
| 63 | + if (apiFileExists) { |
| 64 | + const contents = (await fs.readFile(apiFilePath)).toString(); |
| 65 | + const updated = contents.replace(HARDCODED_API_URL, REPLACED_API_URL); |
| 66 | + if (updated !== contents) { |
| 67 | + await writeFile(apiFilePath, updated, { format: true }); |
| 68 | + patchedApi = true; |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + const jestConfigPath = path.join(projectDir, JEST_CONFIG_PATH); |
| 73 | + const jestConfigExists = await fs.pathExists(jestConfigPath); |
| 74 | + let patchedJest = false; |
| 75 | + if (jestConfigExists) { |
| 76 | + const contents = (await fs.readFile(jestConfigPath)).toString(); |
| 77 | + const updated = contents.replace( |
| 78 | + JEST_SETUP_FILES_BEFORE, |
| 79 | + JEST_SETUP_FILES_AFTER, |
| 80 | + ); |
| 81 | + if (updated !== contents) { |
| 82 | + await writeFile(jestConfigPath, updated, { format: true }); |
| 83 | + patchedJest = true; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + await commit('Add environment variable management support.').catch( |
| 88 | + handleCommitError, |
| 89 | + ); |
| 90 | + |
| 91 | + spinner.succeed(`Successfully set up environment variable management! |
| 92 | +
|
| 93 | + What was added: |
| 94 | + - .env.example: Template of environment variables (committed to git) |
| 95 | + - .env: Your local environment variables (gitignored) |
| 96 | + - .env.test: Environment variables for Jest (committed to git) |
| 97 | + - jest.setup.env.js: Loads .env.test before tests run |
| 98 | + - src/config/index.ts: Typed helper to access config values in your app${ |
| 99 | + patchedApi ? `\n - ${API_PATH}: Updated to use EXPO_PUBLIC_API_BASE_URL` : '' |
| 100 | + }${ |
| 101 | + patchedJest |
| 102 | + ? `\n - ${JEST_CONFIG_PATH}: Added setupFiles to load jest.setup.env.js` |
| 103 | + : '' |
| 104 | + } |
| 105 | +
|
| 106 | + Usage in your app: |
| 107 | + import getConfig from 'src/config'; |
| 108 | + const { apiBaseUrl } = getConfig(); |
| 109 | +
|
| 110 | + Variables prefixed with EXPO_PUBLIC_ are automatically loaded by the Expo CLI |
| 111 | + and inlined into your app bundle — no dotenv or extra config required. |
| 112 | +
|
| 113 | + These values are visible in plain text in the compiled app. |
| 114 | + Never store secrets as EXPO_PUBLIC_ variables. |
| 115 | + `); |
| 116 | +} |
| 117 | + |
| 118 | +async function printIntro() { |
| 119 | + print("Let's set up environment variable management!"); |
| 120 | + print(` |
| 121 | + We will configure your Expo app to handle environment variables using the |
| 122 | + built-in EXPO_PUBLIC_ mechanism. This includes: |
| 123 | +
|
| 124 | + - .env.example: Template showing your environment variables (committed) |
| 125 | + - .env: Your local values (gitignored) |
| 126 | + - .env.test: Environment variables for Jest tests (committed) |
| 127 | + - jest.setup.env.js: Loads .env.test before each test run |
| 128 | + - src/config/index.ts: Typed helper for safe config access |
| 129 | +
|
| 130 | + Variables prefixed with EXPO_PUBLIC_ are automatically loaded by the Expo CLI |
| 131 | + and inlined into the app bundle at build time. No extra packages needed. |
| 132 | + `); |
| 133 | + |
| 134 | + if (!globals.interactive) { |
| 135 | + return; |
| 136 | + } |
| 137 | + |
| 138 | + const proceed = await confirm({ message: 'Ready to proceed?' }); |
| 139 | + if (!proceed) { |
| 140 | + process.exit(0); |
| 141 | + } |
| 142 | + |
| 143 | + print(''); |
| 144 | +} |
| 145 | + |
| 146 | +export default function addEnvAction(...args: unknown[]) { |
| 147 | + const options = (args[0] as unknown[])[0] as Options; |
| 148 | + return addEnv(options); |
| 149 | +} |
0 commit comments