Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
.git/
.planning/
**/.DS_Store
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

A powerful CLI tool to quickly scaffold **React Native** and **Expo** TypeScript projects with best practices.

<p align="center">
<img src="docs/img/terminal-view.png" alt="VoltRN CLI terminal view" width="100%" />
</p>

## Features

- ✅ React Native CLI or Expo support
Expand Down Expand Up @@ -284,9 +288,7 @@ Expected endpoints:

### Documentation

Generated projects with auth flow include:
- `AUTH_FLOW.md` - Complete authentication documentation
- `IMPLEMENTATION_SUMMARY.md` - Implementation details
Generated projects with auth flow include a `README.md` with complete authentication documentation.

## Project Structure

Expand Down
20 changes: 7 additions & 13 deletions cli/setup/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
writeBabelConfig,
} = require('../utils/babel-config');

function setupAuthFlow(projectPath, isExpo, useExpoRouter = false, screenConfig = null) {
async function setupAuthFlow(projectPath, isExpo, useExpoRouter = false, screenConfig = null, pm) {
// Default screen config if none provided
const config = screenConfig || {
hasAuth: true,
Expand All @@ -21,19 +21,13 @@ function setupAuthFlow(projectPath, isExpo, useExpoRouter = false, screenConfig

// Install auth packages
// For Expo Router, we don't need @react-navigation/bottom-tabs since we use expo-router tabs
if (isExpo) {
if (useExpoRouter) {
executeCommand(
'npm install @forward-software/react-auth axios jwt-check-expiry --legacy-peer-deps'
);
} else {
executeCommand(
'npm install @forward-software/react-auth @react-navigation/bottom-tabs axios jwt-check-expiry --legacy-peer-deps'
);
}
if (useExpoRouter) {
await executeCommand(
pm.add('@forward-software/react-auth axios jwt-check-expiry')
);
} else {
executeCommand(
'npm install @forward-software/react-auth @react-navigation/bottom-tabs axios jwt-check-expiry'
await executeCommand(
pm.add('@forward-software/react-auth @react-navigation/bottom-tabs axios jwt-check-expiry')
);
}

Expand Down
20 changes: 10 additions & 10 deletions cli/setup/bootsplash.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ const { log } = require('../utils/logger');
* @param {boolean} useExpoRouter - Whether the project uses Expo Router
* @param {boolean} useAuthFlow - Whether auth flow is enabled
*/
function setupBootsplash(projectPath, isExpo, useExpoRouter, useAuthFlow) {
async function setupBootsplash(projectPath, isExpo, useExpoRouter, useAuthFlow, pm) {
log.info('Installing react-native-bootsplash and react-native-toolbox...');

// Install packages
executeCommand('npm install react-native-bootsplash', { cwd: projectPath });
executeCommand(
'npm install --save-dev @forward-software/react-native-toolbox',
await executeCommand(pm.add('react-native-bootsplash'), { cwd: projectPath });
await executeCommand(
pm.addDev('@forward-software/react-native-toolbox'),
{ cwd: projectPath },
);

Expand Down Expand Up @@ -46,8 +46,8 @@ function setupBootsplash(projectPath, isExpo, useExpoRouter, useAuthFlow) {

// Run initial splash and icon generation (RN CLI only. Expo does it during prebuild)
if (!isExpo) {
runSplashGeneration(projectPath);
runIconGeneration(projectPath);
await runSplashGeneration(projectPath);
await runIconGeneration(projectPath);
}

// Wire up BootSplash.hide() in the app entry point
Expand Down Expand Up @@ -120,9 +120,9 @@ function configureExpoPlugin(projectPath) {
/**
* Run the initial splash screen generation for RN CLI projects
*/
function runSplashGeneration(projectPath) {
async function runSplashGeneration(projectPath) {
try {
executeCommand(
await executeCommand(
'npx react-native-bootsplash generate assets/splashscreen.svg --platforms=android,ios --background=1A1A2E --logo-width=128 --assets-output=assets/bootsplash',
{ cwd: projectPath },
);
Expand All @@ -137,9 +137,9 @@ function runSplashGeneration(projectPath) {
/**
* Run the initial app icon generation for RN CLI projects
*/
function runIconGeneration(projectPath) {
async function runIconGeneration(projectPath) {
try {
executeCommand(
await executeCommand(
'npx @forward-software/react-native-toolbox icons assets/icon.svg',
{ cwd: projectPath },
);
Expand Down
22 changes: 8 additions & 14 deletions cli/setup/framework.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
const { executeCommand } = require('../utils/commands');
const { log } = require('../utils/logger');

function createProject(projectName, isExpo) {
log.step(
`Creating ${isExpo ? 'Expo' : 'React Native'} TypeScript project...`
);
async function createProject(projectName, isExpo) {
if (isExpo) {
executeCommand(
`npx create-expo-app@latest ${projectName} --template blank-typescript`
await executeCommand(
`npx create-expo-app@latest ${projectName} --template blank-typescript`,
);
} else {
executeCommand(
`npx @react-native-community/cli@latest init ${projectName}`
await executeCommand(
`npx @react-native-community/cli@latest init ${projectName}`,
);
}
}

function installIOSDependencies(projectPath) {
async function installIOSDependencies(projectPath) {
const path = require('path');
const fs = require('fs');

log.step('Installing iOS dependencies...');
try {
const iosPath = path.join(projectPath, 'ios');
if (fs.existsSync(iosPath)) {
log.info('Running pod install (this may take a few minutes)...');
executeCommand('cd ios && pod install && cd ..', { cwd: projectPath });
log.success('iOS dependencies installed');
await executeCommand('cd ios && pod install && cd ..', { cwd: projectPath });
}
} catch (error) {
log.warning(
'Failed to run pod install. Please run "cd ios && pod install" manually before running on iOS.'
'Failed to run pod install. Please run "cd ios && pod install" manually before running on iOS.',
);
}
}
Expand Down
22 changes: 22 additions & 0 deletions cli/setup/git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { execSync } = require('child_process');
const { log } = require('../utils/logger');

function initGitRepository(projectPath) {
try {
execSync('git init', { cwd: projectPath, stdio: 'pipe' });
execSync('git add .', { cwd: projectPath, stdio: 'pipe' });
execSync('git commit -m "Initial commit via voltrn-cli"', {
cwd: projectPath,
stdio: 'pipe',
});
log.success('Git repository initialized with initial commit');
} catch (error) {
log.warning(
'Could not initialize git repository. Make sure git is installed.'
);
}
}

module.exports = {
initGitRepository,
};
27 changes: 9 additions & 18 deletions cli/setup/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,19 @@ const {
writeBabelConfig,
} = require('../utils/babel-config');

function setupI18n(projectPath, isExpo) {
async function setupI18n(projectPath, isExpo, pm) {
log.info('Installing i18next with TypeScript support...');
await executeCommand(
pm.add('i18next react-i18next react-native-mmkv react-native-nitro-modules')
);
if (isExpo) {
// Use legacy-peer-deps to handle Expo's React version conflicts
executeCommand(
'npm install i18next react-i18next react-native-mmkv react-native-nitro-modules --legacy-peer-deps'
);
executeCommand('npx expo install react-native-localize');
executeCommand(
'npm install --save-dev babel-plugin-module-resolver babel-preset-expo @expo/config-plugins --legacy-peer-deps'
await executeCommand('npx expo install react-native-localize');
await executeCommand(
pm.addDev('babel-plugin-module-resolver babel-preset-expo @expo/config-plugins')
);
} else {
executeCommand(
'npm install i18next react-i18next react-native-mmkv react-native-nitro-modules'
);
executeCommand('npm install react-native-localize');
executeCommand('npm install --save-dev babel-plugin-module-resolver');
await executeCommand(pm.add('react-native-localize'));
await executeCommand(pm.addDev('babel-plugin-module-resolver'));
}

// Ensure src directory exists (for both Expo and React Native CLI)
Expand Down Expand Up @@ -246,11 +242,6 @@ export const switchLocaleTo = (locale: string) => {
tsconfig.compilerOptions = {};
}

// Set baseUrl for better path resolution
if (!tsconfig.compilerOptions.baseUrl) {
tsconfig.compilerOptions.baseUrl = '.';
}

if (!tsconfig.compilerOptions.paths) {
tsconfig.compilerOptions.paths = {};
}
Expand Down
6 changes: 3 additions & 3 deletions cli/setup/license.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const { log } = require('../utils/logger');
*
* @param {string} projectPath - Path to the generated project
*/
function setupLicense(projectPath) {
function setupLicense(projectPath, spinner) {
spinner?.message('Adding MPL-2.0 license...');

// Copy LICENSE file from CLI root to generated project
const sourceLicense = path.join(__dirname, '..', '..', 'LICENSE');
const destLicense = path.join(projectPath, 'LICENSE');
Expand All @@ -29,8 +31,6 @@ function setupLicense(projectPath) {
pkg.license = 'MPL-2.0';
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
}

log.success('MPL-2.0 license added');
}

module.exports = { setupLicense };
41 changes: 15 additions & 26 deletions cli/setup/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
createDynamicScreens,
} = require('./dynamic-screens');

function setupReactNavigation(projectPath, isExpo, useI18n, screenConfig = null, useTheme = false) {
async function setupReactNavigation(projectPath, isExpo, useI18n, screenConfig = null, useTheme = false, pm) {
log.info('Installing React Navigation...');

const config = screenConfig || {
Expand All @@ -29,19 +29,16 @@ function setupReactNavigation(projectPath, isExpo, useI18n, screenConfig = null,
const pattern = config.navigationPattern || NAVIGATION_PATTERNS.STACK;

// Base React Navigation packages
await executeCommand(
pm.add('@react-navigation/native @react-navigation/native-stack')
);
if (isExpo) {
executeCommand(
'npm install @react-navigation/native @react-navigation/native-stack --legacy-peer-deps'
);
executeCommand(
await executeCommand(
'npx expo install react-native-screens react-native-safe-area-context'
);
} else {
executeCommand(
'npm install @react-navigation/native @react-navigation/native-stack'
);
executeCommand(
'npm install react-native-screens react-native-safe-area-context'
await executeCommand(
pm.add('react-native-screens react-native-safe-area-context')
);
}

Expand All @@ -50,29 +47,21 @@ function setupReactNavigation(projectPath, isExpo, useI18n, screenConfig = null,
pattern === NAVIGATION_PATTERNS.TABS ||
pattern === NAVIGATION_PATTERNS.TABS_DRAWER
) {
if (isExpo) {
executeCommand(
'npm install @react-navigation/bottom-tabs --legacy-peer-deps'
);
} else {
executeCommand('npm install @react-navigation/bottom-tabs');
}
await executeCommand(pm.add('@react-navigation/bottom-tabs'));
}

if (
pattern === NAVIGATION_PATTERNS.DRAWER ||
pattern === NAVIGATION_PATTERNS.TABS_DRAWER
) {
await executeCommand(pm.add('@react-navigation/drawer'));
if (isExpo) {
executeCommand(
'npm install @react-navigation/drawer --legacy-peer-deps'
);
executeCommand(
await executeCommand(
'npx expo install react-native-gesture-handler react-native-reanimated react-native-worklets'
);
} else {
executeCommand(
'npm install @react-navigation/drawer react-native-gesture-handler react-native-reanimated react-native-worklets'
await executeCommand(
pm.add('react-native-gesture-handler react-native-reanimated react-native-worklets')
);
}

Expand Down Expand Up @@ -135,9 +124,9 @@ function setupReactNavigation(projectPath, isExpo, useI18n, screenConfig = null,
}
}

function setupExpoRouter(projectPath, useI18n, useAuthFlow = false, screenConfig = null, useTheme = false) {
async function setupExpoRouter(projectPath, useI18n, useAuthFlow = false, screenConfig = null, useTheme = false, pm) {
log.info('Installing Expo Router...');
executeCommand(
await executeCommand(
'npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar react-dom'
);

Expand Down Expand Up @@ -167,7 +156,7 @@ function setupExpoRouter(projectPath, useI18n, useAuthFlow = false, screenConfig
pattern === NAVIGATION_PATTERNS.DRAWER ||
pattern === NAVIGATION_PATTERNS.TABS_DRAWER
) {
executeCommand(
await executeCommand(
'npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated react-native-worklets'
);

Expand Down
Loading