Skip to content

Commit 0d8f2e4

Browse files
Esemesekthymikee
authored andcommitted
feat: add React logo and spinners to make init UI nicer (#292)
* Implement spinners for initializing flow * Use execa module * Change banner * Ignore silent flag when in verbose mode * Refactor packageManager test * Change loader exports * Fix e2e test * Rename packageManager * Add e2e test * smaller paddings for React logo * Cleanup tests * fix flowfixme * add space for post install script * rename packageManager.test as well
1 parent 323b9a2 commit 0d8f2e4

File tree

18 files changed

+297
-110
lines changed

18 files changed

+297
-110
lines changed

e2e/__tests__/init.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ test('init --template', () => {
5050
'TestInit',
5151
]);
5252

53-
expect(stdout).toContain('Initializing new project from external template');
53+
expect(stdout).toContain('Welcome to React Native!');
5454
expect(stdout).toContain('Run instructions');
5555

5656
// make sure we don't leave garbage

packages/cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"node-fetch": "^2.2.0",
4949
"node-notifier": "^5.2.1",
5050
"opn": "^3.0.2",
51+
"ora": "^3.4.0",
5152
"plist": "^3.0.0",
5253
"semver": "^5.0.3",
5354
"serve-static": "^1.13.1",

packages/cli/src/cliEntry.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {getCommands} from './commands';
1818
import init from './commands/init/initCompat';
1919
import assertRequiredOptions from './tools/assertRequiredOptions';
2020
import logger from './tools/logger';
21-
import {setProjectDir} from './tools/PackageManager';
21+
import {setProjectDir} from './tools/packageManager';
2222
import pkgJson from '../package.json';
2323
import loadConfig from './tools/config';
2424

packages/cli/src/commands/init/__tests__/template.test.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// @flow
2+
jest.mock('execa', () => jest.fn());
3+
import execa from 'execa';
24
import path from 'path';
3-
import ChildProcess from 'child_process';
4-
import * as PackageManger from '../../../tools/PackageManager';
5+
import * as PackageManger from '../../../tools/packageManager';
56
import {
67
installTemplatePackage,
78
getTemplateConfig,
@@ -14,15 +15,17 @@ const TEMPLATE_NAME = 'templateName';
1415

1516
afterEach(() => {
1617
jest.restoreAllMocks();
18+
jest.clearAllMocks();
1719
});
1820

19-
test('installTemplatePackage', () => {
21+
test('installTemplatePackage', async () => {
2022
jest.spyOn(PackageManger, 'install').mockImplementationOnce(() => {});
2123

22-
installTemplatePackage(TEMPLATE_NAME, true);
24+
await installTemplatePackage(TEMPLATE_NAME, true);
2325

2426
expect(PackageManger.install).toHaveBeenCalledWith([TEMPLATE_NAME], {
2527
preferYarn: false,
28+
silent: true,
2629
});
2730
});
2831

@@ -68,21 +71,20 @@ test('copyTemplate', () => {
6871
expect(copyFiles.default).toHaveBeenCalledWith(expect.any(String), CWD);
6972
});
7073

71-
test('executePostInitScript', () => {
74+
test('executePostInitScript', async () => {
7275
const RESOLVED_PATH = '/some/path/script.js';
7376
const SCRIPT_PATH = './script.js';
7477

7578
jest.spyOn(path, 'resolve').mockImplementationOnce(() => RESOLVED_PATH);
76-
jest.spyOn(ChildProcess, 'execFileSync').mockImplementationOnce(() => {});
7779

78-
executePostInitScript(TEMPLATE_NAME, SCRIPT_PATH);
80+
await executePostInitScript(TEMPLATE_NAME, SCRIPT_PATH);
7981

8082
expect(path.resolve).toHaveBeenCalledWith(
8183
'node_modules',
8284
TEMPLATE_NAME,
8385
SCRIPT_PATH,
8486
);
85-
expect(ChildProcess.execFileSync).toHaveBeenCalledWith(RESOLVED_PATH, {
87+
expect(execa).toHaveBeenCalledWith(RESOLVED_PATH, {
8688
stdio: 'inherit',
8789
});
8890
});
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// @flow
2+
import chalk from 'chalk';
3+
4+
const reactLogoArray = [
5+
' ',
6+
' ###### ###### ',
7+
' ### #### #### ### ',
8+
' ## ### ### ## ',
9+
' ## #### ## ',
10+
' ## #### ## ',
11+
' ## ## ## ## ',
12+
' ## ### ### ## ',
13+
' ## ######################## ## ',
14+
' ###### ### ### ###### ',
15+
' ### ## ## ## ## ### ',
16+
' ### ## ### #### ### ## ### ',
17+
' ## #### ######## #### ## ',
18+
' ## ### ########## ### ## ',
19+
' ## #### ######## #### ## ',
20+
' ### ## ### #### ### ## ### ',
21+
' ### ## ## ## ## ### ',
22+
' ###### ### ### ###### ',
23+
' ## ######################## ## ',
24+
' ## ### ### ## ',
25+
' ## ## ## ## ',
26+
' ## #### ## ',
27+
' ## #### ## ',
28+
' ## ### ### ## ',
29+
' ### #### #### ### ',
30+
' ###### ###### ',
31+
' ',
32+
];
33+
34+
const welcomeMessage =
35+
' Welcome to React Native! ';
36+
const learnOnceMessage =
37+
' Learn Once Write Anywhere ';
38+
39+
export default `${chalk.blue(reactLogoArray.join('\n'))}
40+
41+
${chalk.yellow.bold(welcomeMessage)}
42+
${chalk.gray(learnOnceMessage)}
43+
`;

packages/cli/src/commands/init/init.js

+79-28
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import {
1414
executePostInitScript,
1515
} from './template';
1616
import {changePlaceholderInTemplate} from './editTemplate';
17-
import * as PackageManager from '../../tools/PackageManager';
17+
import * as PackageManager from '../../tools/packageManager';
1818
import {processTemplateName} from './templateName';
19+
import banner from './banner';
20+
import {getLoader} from '../../tools/loader';
1921

2022
type Options = {|
2123
template?: string,
@@ -39,50 +41,99 @@ async function createFromExternalTemplate(
3941
templateName: string,
4042
npm?: boolean,
4143
) {
42-
logger.info('Initializing new project from external template');
44+
logger.debug('Initializing new project from external template');
45+
logger.log(banner);
4346

44-
let {uri, name} = await processTemplateName(templateName);
47+
const Loader = getLoader();
4548

46-
installTemplatePackage(uri, npm);
47-
name = adjustNameIfUrl(name);
48-
const templateConfig = getTemplateConfig(name);
49-
copyTemplate(name, templateConfig.templateDir);
50-
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);
49+
const loader = new Loader({text: 'Downloading template'});
50+
loader.start();
5151

52-
if (templateConfig.postInitScript) {
53-
executePostInitScript(name, templateConfig.postInitScript);
54-
}
52+
try {
53+
let {uri, name} = await processTemplateName(templateName);
54+
55+
await installTemplatePackage(uri, npm);
56+
loader.succeed();
57+
loader.start('Copying template');
58+
59+
name = adjustNameIfUrl(name);
60+
const templateConfig = getTemplateConfig(name);
61+
copyTemplate(name, templateConfig.templateDir);
62+
63+
loader.succeed();
64+
loader.start('Preparing template');
5565

56-
PackageManager.installAll({preferYarn: !npm});
66+
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);
67+
68+
loader.succeed();
69+
const {postInitScript} = templateConfig;
70+
if (postInitScript) {
71+
// Leaving trailing space because there may be stdout from the script
72+
loader.start('Executing post init script ');
73+
await executePostInitScript(name, postInitScript);
74+
loader.succeed();
75+
}
76+
77+
loader.start('Installing all required dependencies');
78+
await PackageManager.installAll({preferYarn: !npm, silent: true});
79+
loader.succeed();
80+
} catch (e) {
81+
loader.fail();
82+
throw new Error(e);
83+
}
5784
}
5885

5986
async function createFromReactNativeTemplate(
6087
projectName: string,
6188
version: string,
6289
npm?: boolean,
6390
) {
64-
logger.info('Initializing new project');
91+
logger.debug('Initializing new project');
92+
logger.log(banner);
6593

66-
if (semver.valid(version) && !semver.satisfies(version, '0.60.0')) {
67-
throw new Error(
68-
'Cannot use React Native CLI to initialize project with version less than 0.60.0',
69-
);
70-
}
94+
const Loader = getLoader();
95+
const loader = new Loader({text: 'Downloading template'});
96+
loader.start();
7197

72-
const TEMPLATE_NAME = 'react-native';
98+
try {
99+
if (semver.valid(version) && !semver.gte(version, '0.60.0')) {
100+
throw new Error(
101+
'Cannot use React Native CLI to initialize project with version less than 0.60.0',
102+
);
103+
}
73104

74-
const {uri} = await processTemplateName(`${TEMPLATE_NAME}@${version}`);
105+
const TEMPLATE_NAME = 'react-native';
75106

76-
installTemplatePackage(uri, npm);
77-
const templateConfig = getTemplateConfig(TEMPLATE_NAME);
78-
copyTemplate(TEMPLATE_NAME, templateConfig.templateDir);
79-
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);
107+
const {uri} = await processTemplateName(`${TEMPLATE_NAME}@${version}`);
80108

81-
if (templateConfig.postInitScript) {
82-
executePostInitScript(TEMPLATE_NAME, templateConfig.postInitScript);
83-
}
109+
await installTemplatePackage(uri, npm);
110+
111+
loader.succeed();
112+
loader.start('Copying template');
113+
114+
const templateConfig = getTemplateConfig(TEMPLATE_NAME);
115+
copyTemplate(TEMPLATE_NAME, templateConfig.templateDir);
84116

85-
PackageManager.installAll({preferYarn: !npm});
117+
loader.succeed();
118+
loader.start('Processing template');
119+
120+
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);
121+
122+
loader.succeed();
123+
const {postInitScript} = templateConfig;
124+
if (postInitScript) {
125+
loader.start('Executing post init script');
126+
await executePostInitScript(TEMPLATE_NAME, postInitScript);
127+
loader.succeed();
128+
}
129+
130+
loader.start('Installing all required dependencies');
131+
await PackageManager.installAll({preferYarn: !npm, silent: true});
132+
loader.succeed();
133+
} catch (e) {
134+
loader.fail();
135+
throw new Error(e);
136+
}
86137
}
87138

88139
function createProject(projectName: string, options: Options, version: string) {

packages/cli/src/commands/init/initCompat.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import path from 'path';
1313
import process from 'process';
1414
import printRunInstructions from './printRunInstructions';
1515
import {createProjectFromTemplate} from '../../tools/generator/templates';
16-
import * as PackageManager from '../../tools/PackageManager';
16+
import * as PackageManager from '../../tools/packageManager';
1717
import logger from '../../tools/logger';
1818

1919
/**
@@ -25,7 +25,7 @@ import logger from '../../tools/logger';
2525
* @param options Command line options passed from the react-native-cli directly.
2626
* E.g. `{ version: '0.43.0', template: 'navigation' }`
2727
*/
28-
function initCompat(projectDir, argsOrName) {
28+
async function initCompat(projectDir, argsOrName) {
2929
const args = Array.isArray(argsOrName)
3030
? argsOrName // argsOrName was e.g. ['AwesomeApp', '--verbose']
3131
: [argsOrName].concat(process.argv.slice(4)); // argsOrName was e.g. 'AwesomeApp'
@@ -40,31 +40,31 @@ function initCompat(projectDir, argsOrName) {
4040
const options = minimist(args);
4141

4242
logger.info(`Setting up new React Native app in ${projectDir}`);
43-
generateProject(projectDir, newProjectName, options);
43+
await generateProject(projectDir, newProjectName, options);
4444
}
4545

4646
/**
4747
* Generates a new React Native project based on the template.
4848
* @param Absolute path at which the project folder should be created.
4949
* @param options Command line arguments parsed by minimist.
5050
*/
51-
function generateProject(destinationRoot, newProjectName, options) {
51+
async function generateProject(destinationRoot, newProjectName, options) {
5252
const pkgJson = require('react-native/package.json');
5353
const reactVersion = pkgJson.peerDependencies.react;
5454

55-
PackageManager.setProjectDir(destinationRoot);
56-
createProjectFromTemplate(
55+
await PackageManager.setProjectDir(destinationRoot);
56+
await createProjectFromTemplate(
5757
destinationRoot,
5858
newProjectName,
5959
options.template,
6060
destinationRoot,
6161
);
6262

6363
logger.info('Adding required dependencies');
64-
PackageManager.install([`react@${reactVersion}`]);
64+
await PackageManager.install([`react@${reactVersion}`]);
6565

6666
logger.info('Adding required dev dependencies');
67-
PackageManager.installDev([
67+
await PackageManager.installDev([
6868
'@babel/core',
6969
'@babel/runtime',
7070
'@react-native-community/eslint-config',

packages/cli/src/commands/init/template.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// @flow
2-
import {execFileSync} from 'child_process';
2+
3+
import execa from 'execa';
34
import path from 'path';
4-
import * as PackageManager from '../../tools/PackageManager';
5+
import * as PackageManager from '../../tools/packageManager';
56
import logger from '../../tools/logger';
67
import copyFiles from '../../tools/copyFiles';
78

@@ -13,7 +14,10 @@ export type TemplateConfig = {
1314

1415
export function installTemplatePackage(templateName: string, npm?: boolean) {
1516
logger.debug(`Installing template from ${templateName}`);
16-
PackageManager.install([templateName], {preferYarn: !npm});
17+
return PackageManager.install([templateName], {
18+
preferYarn: !npm,
19+
silent: true,
20+
});
1721
}
1822

1923
export function getTemplateConfig(templateName: string): TemplateConfig {
@@ -44,5 +48,5 @@ export function executePostInitScript(
4448

4549
logger.debug(`Executing post init script located ${scriptPath}`);
4650

47-
execFileSync(scriptPath, {stdio: 'inherit'});
51+
return execa(scriptPath, {stdio: 'inherit'});
4852
}

packages/cli/src/commands/install/install.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
import type {ContextT} from '../../tools/types.flow';
1111
import logger from '../../tools/logger';
12-
import * as PackageManager from '../../tools/PackageManager';
12+
import * as PackageManager from '../../tools/packageManager';
1313
import link from '../link/link';
1414
import loadConfig from '../../tools/config';
1515

1616
async function install(args: Array<string>, ctx: ContextT) {
1717
const name = args[0];
1818

1919
logger.info(`Installing "${name}"...`);
20-
PackageManager.install([name]);
20+
await PackageManager.install([name]);
2121

2222
// Reload configuration to see newly installed dependency
2323
const newConfig = loadConfig();

packages/cli/src/commands/install/uninstall.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import type {ContextT} from '../../tools/types.flow';
1111
import logger from '../../tools/logger';
12-
import * as PackageManager from '../../tools/PackageManager';
12+
import * as PackageManager from '../../tools/packageManager';
1313
import link from '../link/unlink';
1414

1515
async function uninstall(args: Array<string>, ctx: ContextT) {
@@ -19,7 +19,7 @@ async function uninstall(args: Array<string>, ctx: ContextT) {
1919
await link.func([name], ctx);
2020

2121
logger.info(`Uninstalling "${name}"...`);
22-
PackageManager.uninstall([name]);
22+
await PackageManager.uninstall([name]);
2323

2424
logger.success(`Successfully uninstalled and unlinked "${name}"`);
2525
}

packages/cli/src/commands/upgrade/__tests__/upgrade.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jest.mock(
3434
() => ({name: 'TestApp', dependencies: {'react-native': '^0.57.8'}}),
3535
{virtual: true},
3636
);
37-
jest.mock('../../../tools/PackageManager', () => ({
37+
jest.mock('../../../tools/packageManager', () => ({
3838
install: args => {
3939
mockPushLog('$ yarn add', ...args);
4040
},

0 commit comments

Comments
 (0)