Skip to content

Commit 981e8a2

Browse files
committed
refactor(cli): extract prompt-for-config + reorder fns in init file
1 parent a3cd9cd commit 981e8a2

File tree

3 files changed

+157
-208
lines changed

3 files changed

+157
-208
lines changed

apps/cli/src/commands/add.ts

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
1-
import {
2-
Config,
3-
DEFAULT_COMPONENTS,
4-
DEFAULT_LIB,
5-
getConfig,
6-
rawConfigSchema,
7-
resolveConfigPaths,
8-
} from '@/src/utils/get-config';
1+
import { Config, getConfig } from '@/src/utils/get-config';
92
import { getPackageManager } from '@/src/utils/get-package-manager';
103
import { handleError } from '@/src/utils/handle-error';
114
import { logger } from '@/src/utils/logger';
5+
import { promptForConfig } from '@/src/utils/prompt-for-config';
126
import chalk from 'chalk';
137
import { Command } from 'commander';
148
import { execa } from 'execa';
159
import { existsSync, promises as fs } from 'fs';
1610
import ora, { Ora } from 'ora';
1711
import path from 'path';
1812
import prompts from 'prompts';
13+
import { fileURLToPath } from 'url';
1914
import { z } from 'zod';
2015
import { Component, INVALID_COMPONENT_ERROR, getAllComponentsToWrite } from '../items';
2116
import { COMPONENTS } from '../items/components';
22-
import { fileURLToPath } from 'url';
2317

2418
const filePath = fileURLToPath(import.meta.url);
2519
const fileDir = path.dirname(filePath);
@@ -242,47 +236,3 @@ function fixImports(rawfile: string, componentsAlias: string, libAlias: string)
242236
.replaceAll('../../components', componentsAlias)
243237
.replaceAll('../../lib', libAlias);
244238
}
245-
246-
async function promptForConfig(cwd: string) {
247-
const highlight = (text: string) => chalk.cyan(text);
248-
249-
const options = await prompts([
250-
{
251-
type: 'text',
252-
name: 'components',
253-
message: `Configure the import alias for ${highlight('components')}:`,
254-
initial: DEFAULT_COMPONENTS,
255-
},
256-
{
257-
type: 'text',
258-
name: 'lib',
259-
message: `Configure the import alias for ${highlight('lib')}:`,
260-
initial: DEFAULT_LIB,
261-
},
262-
]);
263-
264-
const config = rawConfigSchema.parse({
265-
aliases: {
266-
lib: options.lib,
267-
components: options.components,
268-
},
269-
});
270-
271-
const { proceed } = await prompts({
272-
type: 'confirm',
273-
name: 'proceed',
274-
message: `Write configuration to ${highlight('components.json')}. Proceed?`,
275-
initial: true,
276-
});
277-
278-
if (proceed) {
279-
// Write to file.
280-
logger.info('');
281-
const spinner = ora(`Writing components.json...`).start();
282-
const targetPath = path.resolve(cwd, 'components.json');
283-
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8');
284-
spinner.succeed();
285-
}
286-
287-
return await resolveConfigPaths(cwd, config);
288-
}

apps/cli/src/commands/init.ts

Lines changed: 99 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
import {
2-
DEFAULT_COMPONENTS,
3-
DEFAULT_LIB,
4-
getConfig,
5-
rawConfigSchema,
6-
resolveConfigPaths,
7-
} from '@/src/utils/get-config';
1+
import { getConfig } from '@/src/utils/get-config';
82
import { handleError } from '@/src/utils/handle-error';
93
import { logger } from '@/src/utils/logger';
4+
import { promptForConfig } from '@/src/utils/prompt-for-config';
105
import chalk from 'chalk';
116
import { execSync } from 'child_process';
127
import { Command } from 'commander';
@@ -22,11 +17,6 @@ import { z } from 'zod';
2217
const filePath = fileURLToPath(import.meta.url);
2318
const fileDir = path.dirname(filePath);
2419

25-
const initOptionsSchema = z.object({
26-
cwd: z.string(),
27-
overwrite: z.boolean(),
28-
});
29-
3020
const REQUIRED_DEPENDENCIES = [
3121
'nativewind',
3222
'expo-navigation-bar',
@@ -52,68 +42,117 @@ const TEMPLATE_FILES = [
5242
'lib/icons/iconWithClassName.ts',
5343
] as const;
5444

55-
async function installDependencies(cwd: string, spinner: Ora) {
56-
try {
57-
spinner.text = 'Installing dependencies...';
58-
await execa('npx', ['expo', 'install', ...REQUIRED_DEPENDENCIES], {
59-
cwd,
60-
stdio: 'inherit',
45+
const initOptionsSchema = z.object({
46+
cwd: z.string(),
47+
overwrite: z.boolean(),
48+
});
49+
50+
export const init = new Command()
51+
.name('init')
52+
.description('Initialize the required configuration for your React Native project')
53+
.option(
54+
'-c, --cwd <cwd>',
55+
'the working directory. defaults to the current directory.',
56+
process.cwd()
57+
)
58+
.option('-o, --overwrite', 'overwrite existing files', false)
59+
.action(async (opts) => {
60+
try {
61+
const options = initOptionsSchema.parse(opts);
62+
const cwd = path.resolve(options.cwd);
63+
64+
await validateProjectDirectory(cwd);
65+
await checkGitStatus(cwd);
66+
await initializeProject(cwd, options.overwrite);
67+
} catch (error) {
68+
handleError(error);
69+
}
70+
});
71+
72+
async function validateProjectDirectory(cwd: string) {
73+
if (!existsSync(cwd)) {
74+
logger.error(`The path ${cwd} does not exist. Please try again.`);
75+
process.exit(1);
76+
}
77+
78+
if (!existsSync(path.join(cwd, 'package.json'))) {
79+
logger.error(
80+
'No package.json found. Please run this command in a React Native project directory.'
81+
);
82+
process.exit(1);
83+
}
84+
}
85+
86+
async function checkGitStatus(cwd: string) {
87+
if (await shouldPromptGitWarning(cwd)) {
88+
const { proceed } = await prompts({
89+
type: 'confirm',
90+
name: 'proceed',
91+
message:
92+
'The Git repository is dirty (uncommitted changes). It is recommended to commit your changes before proceeding. Do you want to continue?',
93+
initial: false,
6194
});
62-
spinner.text = 'Dependencies installed successfully';
95+
96+
if (!proceed) {
97+
logger.info('Installation cancelled.');
98+
process.exit(0);
99+
}
100+
}
101+
}
102+
103+
async function shouldPromptGitWarning(cwd: string): Promise<boolean> {
104+
try {
105+
execSync('git rev-parse --is-inside-work-tree', { cwd });
106+
const status = execSync('git status --porcelain', { cwd }).toString();
107+
return !!status;
63108
} catch (error) {
64-
spinner.fail('Failed to install dependencies');
65-
handleError(error);
66-
process.exit(1);
109+
return false;
67110
}
68111
}
69112

70-
async function promptForConfig(cwd: string) {
71-
const highlight = (text: string) => chalk.cyan(text);
113+
async function initializeProject(cwd: string, overwrite: boolean) {
114+
const spinner = ora(`Initializing project...`).start();
72115

73116
try {
74-
const options = await prompts([
75-
{
76-
type: 'text',
77-
name: 'components',
78-
message: `Configure the import alias for ${highlight('components')}:`,
79-
initial: DEFAULT_COMPONENTS,
80-
},
81-
{
82-
type: 'text',
83-
name: 'lib',
84-
message: `Configure the import alias for ${highlight('lib')}:`,
85-
initial: DEFAULT_LIB,
86-
},
87-
]);
117+
let config = await getConfig(cwd);
88118

89-
const components = options.components || DEFAULT_COMPONENTS;
90-
const lib = options.lib || DEFAULT_LIB;
119+
if (!config) {
120+
spinner.stop();
121+
config = await promptForConfig(cwd);
122+
spinner.start();
123+
}
91124

92-
const config = rawConfigSchema.parse({
93-
aliases: {
94-
components,
95-
lib,
96-
},
97-
});
125+
const templatesDir = path.join(fileDir, '../__generated/starter-base');
98126

99-
const { proceed } = await prompts({
100-
type: 'confirm',
101-
name: 'proceed',
102-
message: `Write configuration to ${highlight('components.json')}. Proceed?`,
103-
initial: true,
104-
});
127+
await installDependencies(cwd, spinner);
128+
await updateTsConfig(cwd, config, spinner);
105129

106-
if (proceed) {
107-
logger.info('');
108-
const spinner = ora(`Writing components.json...`).start();
109-
const targetPath = path.resolve(cwd, 'components.json');
110-
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8');
111-
spinner.succeed();
130+
spinner.text = 'Adding config and utility files...';
131+
for (const file of TEMPLATE_FILES) {
132+
await copyTemplateFile(file, templatesDir, cwd, spinner, overwrite);
112133
}
113134

114-
return await resolveConfigPaths(cwd, config);
135+
await updateLayoutFile(cwd, spinner);
136+
137+
spinner.succeed('Initialization completed successfully!');
115138
} catch (error) {
116-
logger.error('Failed to configure project.');
139+
spinner.fail('Initialization failed');
140+
handleError(error);
141+
process.exit(1);
142+
}
143+
}
144+
145+
async function installDependencies(cwd: string, spinner: Ora) {
146+
try {
147+
spinner.text = 'Installing dependencies...';
148+
await execa('npx', ['expo', 'install', ...REQUIRED_DEPENDENCIES], {
149+
cwd,
150+
stdio: 'inherit',
151+
});
152+
spinner.text = 'Dependencies installed successfully';
153+
} catch (error) {
154+
spinner.fail('Failed to install dependencies');
155+
handleError(error);
117156
process.exit(1);
118157
}
119158
}
@@ -229,98 +268,3 @@ async function updateLayoutFile(cwd: string, spinner: Ora) {
229268
handleError(error);
230269
}
231270
}
232-
233-
async function shouldPromptGitWarning(cwd: string): Promise<boolean> {
234-
try {
235-
execSync('git rev-parse --is-inside-work-tree', { cwd });
236-
const status = execSync('git status --porcelain', { cwd }).toString();
237-
return !!status;
238-
} catch (error) {
239-
return false;
240-
}
241-
}
242-
243-
async function validateProjectDirectory(cwd: string) {
244-
if (!existsSync(cwd)) {
245-
logger.error(`The path ${cwd} does not exist. Please try again.`);
246-
process.exit(1);
247-
}
248-
249-
if (!existsSync(path.join(cwd, 'package.json'))) {
250-
logger.error(
251-
'No package.json found. Please run this command in a React Native project directory.'
252-
);
253-
process.exit(1);
254-
}
255-
}
256-
257-
async function checkGitStatus(cwd: string) {
258-
if (await shouldPromptGitWarning(cwd)) {
259-
const { proceed } = await prompts({
260-
type: 'confirm',
261-
name: 'proceed',
262-
message:
263-
'The Git repository is dirty (uncommitted changes). It is recommended to commit your changes before proceeding. Do you want to continue?',
264-
initial: false,
265-
});
266-
267-
if (!proceed) {
268-
logger.info('Installation cancelled.');
269-
process.exit(0);
270-
}
271-
}
272-
}
273-
274-
async function initializeProject(cwd: string, overwrite: boolean) {
275-
const spinner = ora(`Initializing project...`).start();
276-
277-
try {
278-
let config = await getConfig(cwd);
279-
280-
if (!config) {
281-
spinner.stop();
282-
config = await promptForConfig(cwd);
283-
spinner.start();
284-
}
285-
286-
const templatesDir = path.join(fileDir, '../__generated/starter-base');
287-
288-
await installDependencies(cwd, spinner);
289-
await updateTsConfig(cwd, config, spinner);
290-
291-
spinner.text = 'Adding config and utility files...';
292-
for (const file of TEMPLATE_FILES) {
293-
await copyTemplateFile(file, templatesDir, cwd, spinner, overwrite);
294-
}
295-
296-
await updateLayoutFile(cwd, spinner);
297-
298-
spinner.succeed('Initialization completed successfully!');
299-
} catch (error) {
300-
spinner.fail('Initialization failed');
301-
handleError(error);
302-
process.exit(1);
303-
}
304-
}
305-
306-
export const init = new Command()
307-
.name('init')
308-
.description('Initialize the required configuration for your React Native project')
309-
.option(
310-
'-c, --cwd <cwd>',
311-
'the working directory. defaults to the current directory.',
312-
process.cwd()
313-
)
314-
.option('-o, --overwrite', 'overwrite existing files', false)
315-
.action(async (opts) => {
316-
try {
317-
const options = initOptionsSchema.parse(opts);
318-
const cwd = path.resolve(options.cwd);
319-
320-
await validateProjectDirectory(cwd);
321-
await checkGitStatus(cwd);
322-
await initializeProject(cwd, options.overwrite);
323-
} catch (error) {
324-
handleError(error);
325-
}
326-
});

0 commit comments

Comments
 (0)