Skip to content

Commit 7d8b0f6

Browse files
unisol1020mrzachnugent
authored andcommitted
Add init command for project setup
1 parent 89af21d commit 7d8b0f6

File tree

5 files changed

+417
-120
lines changed

5 files changed

+417
-120
lines changed

apps/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@react-native-reusables/cli",
3-
"version": "0.1.2",
3+
"version": "0.2.0",
44
"description": "Add react-native-reusables to your project.",
55
"publishConfig": {
66
"access": "public"

apps/cli/src/commands/init.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { existsSync } from 'fs';
2+
import { promises as fs } from 'fs';
3+
import { Command } from 'commander';
4+
import { execa } from 'execa';
5+
import ora, { Ora } from 'ora';
6+
import path from 'path';
7+
import { z } from 'zod';
8+
import { handleError } from '@/src/utils/handle-error';
9+
import { logger } from '@/src/utils/logger';
10+
import { fileURLToPath } from 'url';
11+
import chalk from 'chalk';
12+
import prompts from 'prompts';
13+
import glob from 'fast-glob';
14+
15+
const filePath = fileURLToPath(import.meta.url);
16+
const fileDir = path.dirname(filePath);
17+
18+
const initOptionsSchema = z.object({
19+
cwd: z.string(),
20+
overwrite: z.boolean(),
21+
});
22+
23+
const REQUIRED_DEPENDENCIES = [
24+
'nativewind',
25+
'expo-navigation-bar',
26+
'tailwindcss-animate',
27+
'@react-native-async-storage/async-storage',
28+
'class-variance-authority',
29+
'clsx',
30+
'tailwind-merge',
31+
'react-native-svg',
32+
'lucide-react-native',
33+
'@rn-primitives/portal',
34+
] as const;
35+
36+
const TEMPLATE_FILES = [
37+
'tailwind.config.js',
38+
'nativewind-env.d.ts',
39+
'global.css',
40+
'babel.config.js',
41+
'metro.config.js',
42+
'lib/utils.ts',
43+
'lib/useColorScheme.tsx',
44+
'lib/constants.ts',
45+
'lib/android-navigation-bar.ts',
46+
'lib/icons/iconWithClassName.ts',
47+
] as const;
48+
49+
async function installDependencies(cwd: string, spinner: Ora) {
50+
try {
51+
spinner.text = 'Installing dependencies...';
52+
await execa('npx', ['expo', 'install', ...REQUIRED_DEPENDENCIES], {
53+
cwd,
54+
stdio: 'inherit',
55+
});
56+
} catch (error) {
57+
spinner.fail('Failed to install dependencies');
58+
handleError(error);
59+
}
60+
}
61+
62+
async function updateTsConfig(cwd: string, spinner: Ora) {
63+
try {
64+
spinner.text = 'Configuring path aliases...';
65+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
66+
const tsconfig = existsSync(tsconfigPath)
67+
? JSON.parse(await fs.readFile(tsconfigPath, 'utf8'))
68+
: {};
69+
70+
tsconfig.compilerOptions = {
71+
...tsconfig.compilerOptions,
72+
baseUrl: '.',
73+
paths: {
74+
'~/*': ['*'],
75+
...tsconfig.compilerOptions?.paths,
76+
},
77+
};
78+
79+
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
80+
} catch (error) {
81+
spinner.fail('Failed to update tsconfig.json');
82+
handleError(error);
83+
}
84+
}
85+
86+
async function copyTemplateFile(
87+
file: string,
88+
templatesDir: string,
89+
targetDir: string,
90+
spinner: Ora,
91+
overwriteFlag: boolean
92+
) {
93+
const targetPath = path.join(targetDir, file);
94+
spinner.stop();
95+
96+
if (existsSync(targetPath)) {
97+
if (!overwriteFlag) {
98+
logger.info(
99+
`File already exists: ${chalk.bgCyan(
100+
file
101+
)} was skipped. To overwrite, run with the ${chalk.green('--overwrite')} flag.`
102+
);
103+
return;
104+
}
105+
106+
const { overwrite } = await prompts({
107+
type: 'confirm',
108+
name: 'overwrite',
109+
message: `File already exists: ${chalk.yellow(file)}. Would you like to overwrite?`,
110+
initial: false,
111+
});
112+
113+
if (!overwrite) {
114+
logger.info(`Skipped`);
115+
return;
116+
}
117+
}
118+
119+
spinner.start(`Installing ${file}...`);
120+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
121+
await fs.copyFile(path.join(templatesDir, file), targetPath);
122+
}
123+
124+
async function updateLayoutFile(cwd: string, spinner: Ora) {
125+
try {
126+
spinner.text = 'Updating layout file...';
127+
const layoutFiles = await glob(
128+
['app/_layout.{ts,tsx,js,jsx}', '(app)/_layout.{ts,tsx,js,jsx}'],
129+
{
130+
cwd,
131+
ignore: ['node_modules/**'],
132+
}
133+
);
134+
135+
if (!layoutFiles.length) {
136+
spinner.warn('No _layout file found in app directory');
137+
return;
138+
}
139+
140+
const layoutPath = path.join(cwd, layoutFiles[0]);
141+
const content = await fs.readFile(layoutPath, 'utf8');
142+
143+
if (!content.includes('import "../global.css"')) {
144+
await fs.writeFile(layoutPath, `import "../global.css";\n${content}`);
145+
spinner.succeed(`Updated ${layoutFiles[0]} with global CSS import`);
146+
}
147+
} catch (error) {
148+
spinner.fail('Failed to update layout file');
149+
handleError(error);
150+
}
151+
}
152+
153+
async function updateImportPaths(cwd: string, spinner: Ora) {
154+
try {
155+
spinner.text = 'Updating import paths...';
156+
const files = await glob(['**/*.{ts,tsx,js,jsx}'], {
157+
cwd,
158+
ignore: ['node_modules/**', 'dist/**', 'build/**', '.expo/**'],
159+
});
160+
161+
for (const file of files) {
162+
const filePath = path.join(cwd, file);
163+
const content = await fs.readFile(filePath, 'utf8');
164+
const updatedContent = content
165+
.replace(/(from\s+['"])@\/(.*?['"])/g, '$1~/$2')
166+
.replace(/(import\s+['"])@\/(.*?['"])/g, '$1~/$2');
167+
168+
if (content !== updatedContent) {
169+
await fs.writeFile(filePath, updatedContent);
170+
spinner.text = `Updated imports in ${file}`;
171+
}
172+
}
173+
} catch (error) {
174+
spinner.fail('Failed to update import paths');
175+
handleError(error);
176+
}
177+
}
178+
179+
export const init = new Command()
180+
.name('init')
181+
.description('Initialize the React Native project with required configuration')
182+
.option(
183+
'-c, --cwd <cwd>',
184+
'the working directory. defaults to the current directory.',
185+
process.cwd()
186+
)
187+
.option('-o, --overwrite', 'overwrite existing files', false)
188+
.action(async (opts) => {
189+
try {
190+
const options = initOptionsSchema.parse(opts);
191+
const cwd = path.resolve(options.cwd);
192+
193+
if (!existsSync(cwd)) {
194+
logger.error(`The path ${cwd} does not exist. Please try again.`);
195+
process.exit(1);
196+
}
197+
198+
const spinner = ora(`Initializing project...`).start();
199+
const templatesDir = path.join(fileDir, '../../../packages/templates/starter-base');
200+
201+
await installDependencies(cwd, spinner);
202+
await updateTsConfig(cwd, spinner);
203+
204+
spinner.text = 'Copying template files...';
205+
for (const file of TEMPLATE_FILES) {
206+
await copyTemplateFile(file, templatesDir, cwd, spinner, options.overwrite);
207+
}
208+
209+
await updateImportPaths(cwd, spinner);
210+
await updateLayoutFile(cwd, spinner);
211+
212+
spinner.succeed('Initialization completed successfully!');
213+
} catch (error) {
214+
handleError(error);
215+
}
216+
});

apps/cli/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/usr/bin/env node
22
import { add } from '@/src/commands/add';
3+
import { init } from './commands/init';
4+
35
import { Command } from 'commander';
46

57
import { getPackageInfo } from './utils/get-package-info';
@@ -16,6 +18,7 @@ async function main() {
1618
.version(packageInfo.version || '0.0.0-rc.0', '-v, --version', 'display the version number');
1719

1820
program.addCommand(add);
21+
program.addCommand(init);
1922

2023
program.parse();
2124
}

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@rnr/docs",
33
"type": "module",
4-
"version": "0.0.1",
4+
"version": "0.1.0",
55
"scripts": {
66
"dev": "astro dev",
77
"start": "astro dev",

0 commit comments

Comments
 (0)