Skip to content

Commit 3c86a4b

Browse files
committed
add support for environment variables using Belt CLI
1 parent 696edda commit 3c86a4b

6 files changed

Lines changed: 186 additions & 2 deletions

File tree

src/cli.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ export default function runCli() {
4545
.description('Install and configure Jest and Testing Library')
4646
.action(buildAction(import('./commands/testingLibrary')));
4747

48-
program
48+
const add = program
4949
.command('add')
50-
.description('Add a new feature to your project')
50+
.description('Add a new feature to your project');
51+
52+
add
5153
.command('notifications')
5254
.description(
5355
'Install and configure React Native Firebase with Notifications',
@@ -62,6 +64,17 @@ export default function runCli() {
6264
)
6365
.action(buildAction(import('./commands/notifications')));
6466

67+
add
68+
.command('env')
69+
.description(
70+
'Set up environment variable management with expo-constants and dotenv',
71+
)
72+
.option(
73+
'--no-interactive',
74+
'Pass true to skip all prompts and use default values',
75+
)
76+
.action(buildAction(import('./commands/env')));
77+
6578
printWelcome();
6679
program.parse();
6780
}

src/commands/env.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
}

templates/environments/env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Environment Variables
2+
# Copy this file to .env and fill in your values.
3+
# Only .env.example is committed to version control — never commit .env.
4+
5+
# Variables prefixed with EXPO_PUBLIC_ are automatically loaded by the Expo CLI
6+
# and inlined into your app bundle at build time. They are visible in plain text
7+
# in the compiled app — never store secrets here.
8+
EXPO_PUBLIC_API_BASE_URL=https://api.github.com

templates/environments/env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
EXPO_PUBLIC_API_BASE_URL=https://api.github.com
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
require('dotenv').config({ path: '.env.test' });
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
interface Config {
2+
apiBaseUrl: string;
3+
}
4+
5+
export default function getConfig(): Config {
6+
return {
7+
// process.env.EXPO_PUBLIC_* variables are inlined at build time by the Expo bundler.
8+
// Always use dot notation — bracket notation and destructuring are not supported.
9+
apiBaseUrl: process.env.EXPO_PUBLIC_API_BASE_URL ?? '',
10+
};
11+
}

0 commit comments

Comments
 (0)