Skip to content

feat: add React logo and spinners to make init UI nicer #292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 5, 2019
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"node-fetch": "^2.2.0",
"node-notifier": "^5.2.1",
"opn": "^3.0.2",
"ora": "^3.4.0",
"plist": "^3.0.0",
"semver": "^5.0.3",
"serve-static": "^1.13.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cliEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import init from './commands/init/initCompat';
import assertRequiredOptions from './tools/assertRequiredOptions';
import logger from './tools/logger';
import findPlugins from './tools/findPlugins';
import {setProjectDir} from './tools/PackageManager';
import {setProjectDir} from './tools/packageManager';
import pkgJson from '../package.json';
import loadConfig from './tools/config';

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/init/__tests__/template.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @flow
jest.mock('execa', () => jest.fn());
import execa from 'execa';
import path from 'path';
import ChildProcess from 'child_process';
import * as PackageManger from '../../../tools/PackageManager';
import * as PackageManger from '../../../tools/packageManager';
import {
installTemplatePackage,
getTemplateConfig,
Expand All @@ -16,13 +17,14 @@ afterEach(() => {
jest.restoreAllMocks();
});

test('installTemplatePackage', () => {
test('installTemplatePackage', async () => {
jest.spyOn(PackageManger, 'install').mockImplementationOnce(() => {});

installTemplatePackage(TEMPLATE_NAME, true);
await installTemplatePackage(TEMPLATE_NAME, true);

expect(PackageManger.install).toHaveBeenCalledWith([TEMPLATE_NAME], {
preferYarn: false,
silent: true,
});
});

Expand Down Expand Up @@ -68,21 +70,20 @@ test('copyTemplate', () => {
expect(copyFiles.default).toHaveBeenCalledWith(expect.any(String), CWD);
});

test('executePostInitScript', () => {
test('executePostInitScript', async () => {
const RESOLVED_PATH = '/some/path/script.js';
const SCRIPT_PATH = './script.js';

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

executePostInitScript(TEMPLATE_NAME, SCRIPT_PATH);
await executePostInitScript(TEMPLATE_NAME, SCRIPT_PATH);

expect(path.resolve).toHaveBeenCalledWith(
'node_modules',
TEMPLATE_NAME,
SCRIPT_PATH,
);
expect(ChildProcess.execFileSync).toHaveBeenCalledWith(RESOLVED_PATH, {
expect(execa).toHaveBeenCalledWith(RESOLVED_PATH, {
stdio: 'inherit',
});
});
48 changes: 48 additions & 0 deletions packages/cli/src/commands/init/banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @flow
import chalk from 'chalk';

const reactLogoArray = [
' ',
' ',
' ',
' ###### ###### ',
' ### #### #### ### ',
' ## ### ### ## ',
' ## #### ## ',
' ## #### ## ',
' ## ## ## ## ',
' ## ### ### ## ',
' ## ######################## ## ',
' ###### ### ### ###### ',
' ### ## ## ## ## ### ',
' ### ## ### #### ### ## ### ',
' ## #### ######## #### ## ',
' ## ### ########## ### ## ',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, have you tried if it looks good with 2 spaces padding instead of 4? (similar for the top, I think 1 or 2 is enough)

' ## #### ######## #### ## ',
' ### ## ### #### ### ## ### ',
' ### ## ## ## ## ### ',
' ###### ### ### ###### ',
' ## ######################## ## ',
' ## ### ### ## ',
' ## ## ## ## ',
' ## #### ## ',
' ## #### ## ',
' ## ### ### ## ',
' ### #### #### ### ',
' ###### ###### ',
' ',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to add a very random Guy Fieri at some point behind a flag :trollface:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm down XD

' ',
' ',
' ',
];

const welcomeMessage =
' Welcome to React Native! ';
const learnOnceMessage =
' Learn Once Write Anywhere ';

export default `${chalk.blue(reactLogoArray.join('\n'))}

${chalk.yellow.bold(welcomeMessage)}
${chalk.gray(learnOnceMessage)}
`;
106 changes: 78 additions & 28 deletions packages/cli/src/commands/init/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
executePostInitScript,
} from './template';
import {changePlaceholderInTemplate} from './editTemplate';
import * as PackageManager from '../../tools/PackageManager';
import * as PackageManager from '../../tools/packageManager';
import {processTemplateName} from './templateName';
import banner from './banner';
import getLoader from '../../tools/loader';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we stick to the module name? So loader instead of getLoader

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just replace default export. I think getLoader is good naming here. It shouldn't be confusing when I use import { getLoader } from '../../tools/loader;


type Options = {|
template?: string,
Expand All @@ -39,50 +41,98 @@ async function createFromExternalTemplate(
templateName: string,
npm?: boolean,
) {
logger.info('Initializing new project from external template');
logger.debug('Initializing new project from external template');
logger.log(banner);

let {uri, name} = await processTemplateName(templateName);
const Loader = getLoader();

installTemplatePackage(uri, npm);
name = adjustNameIfUrl(name);
const templateConfig = getTemplateConfig(name);
copyTemplate(name, templateConfig.templateDir);
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);
const loader = new Loader({text: 'Downloading template'});
loader.start();

if (templateConfig.postInitScript) {
executePostInitScript(name, templateConfig.postInitScript);
}
try {
let {uri, name} = await processTemplateName(templateName);

await installTemplatePackage(uri, npm);
loader.succeed();
loader.start('Copying template');

name = adjustNameIfUrl(name);
const templateConfig = getTemplateConfig(name);
copyTemplate(name, templateConfig.templateDir);

loader.succeed();
loader.start('Preparing template');

PackageManager.installAll({preferYarn: !npm});
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);

loader.succeed();
if (templateConfig.postInitScript) {
loader.start('Executing post init script');
// $FlowFixMe
await executePostInitScript(name, templateConfig.postInitScript);
loader.succeed();
}

loader.start('Installing all required dependencies');
Copy link
Member

@thymikee thymikee Apr 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe Installing template dependencies? Whatever sounds better, I'm don't have strong opinion on any

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both sound good :P

await PackageManager.installAll({preferYarn: !npm, silent: true});
loader.succeed();
} catch (e) {
loader.fail();
throw new Error(e);
}
}

async function createFromReactNativeTemplate(
projectName: string,
version: string,
npm?: boolean,
) {
logger.info('Initializing new project');
logger.debug('Initializing new project');
logger.log(banner);

if (semver.valid(version) && !semver.satisfies(version, '0.60.0')) {
throw new Error(
'Cannot use React Native CLI to initialize project with version less than 0.60.0',
);
}
const Loader = getLoader();
const loader = new Loader({text: 'Downloading template'});
loader.start();

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

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

installTemplatePackage(uri, npm);
const templateConfig = getTemplateConfig(TEMPLATE_NAME);
copyTemplate(TEMPLATE_NAME, templateConfig.templateDir);
changePlaceholderInTemplate(projectName, templateConfig.placeholderName);
const {uri} = await processTemplateName(`${TEMPLATE_NAME}@${version}`);

if (templateConfig.postInitScript) {
executePostInitScript(TEMPLATE_NAME, templateConfig.postInitScript);
}
await installTemplatePackage(uri, npm);

loader.succeed();
loader.start('Copying template');

const templateConfig = getTemplateConfig(TEMPLATE_NAME);
copyTemplate(TEMPLATE_NAME, templateConfig.templateDir);

PackageManager.installAll({preferYarn: !npm});
loader.succeed();
loader.start('Processing template');

changePlaceholderInTemplate(projectName, templateConfig.placeholderName);

loader.succeed();
if (templateConfig.postInitScript) {
loader.start('Executing post init script');
// $FlowFixMe
await executePostInitScript(TEMPLATE_NAME, templateConfig.postInitScript);
loader.succeed();
}

loader.start('Installing all required dependencies');
await PackageManager.installAll({preferYarn: !npm, silent: true});
loader.succeed();
} catch (e) {
loader.fail();
throw new Error(e);
}
}

function createProject(projectName: string, options: Options, version: string) {
Expand Down
16 changes: 8 additions & 8 deletions packages/cli/src/commands/init/initCompat.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import path from 'path';
import process from 'process';
import printRunInstructions from './printRunInstructions';
import {createProjectFromTemplate} from '../../tools/generator/templates';
import * as PackageManager from '../../tools/PackageManager';
import * as PackageManager from '../../tools/packageManager';
import logger from '../../tools/logger';

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

logger.info(`Setting up new React Native app in ${projectDir}`);
generateProject(projectDir, newProjectName, options);
await generateProject(projectDir, newProjectName, options);
}

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

PackageManager.setProjectDir(destinationRoot);
createProjectFromTemplate(
await PackageManager.setProjectDir(destinationRoot);
await createProjectFromTemplate(
destinationRoot,
newProjectName,
options.template,
destinationRoot,
);

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

logger.info('Adding required dev dependencies');
PackageManager.installDev([
await PackageManager.installDev([
'@babel/core',
'@babel/runtime',
'@react-native-community/eslint-config',
Expand Down
12 changes: 8 additions & 4 deletions packages/cli/src/commands/init/template.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @flow
import {execFileSync} from 'child_process';

import execa from 'execa';
import path from 'path';
import * as PackageManager from '../../tools/PackageManager';
import * as PackageManager from '../../tools/packageManager';
import logger from '../../tools/logger';
import copyFiles from '../../tools/copyFiles';

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

export function installTemplatePackage(templateName: string, npm?: boolean) {
logger.debug(`Installing template from ${templateName}`);
PackageManager.install([templateName], {preferYarn: !npm});
return PackageManager.install([templateName], {
preferYarn: !npm,
silent: true,
});
}

export function getTemplateConfig(templateName: string): TemplateConfig {
Expand Down Expand Up @@ -44,5 +48,5 @@ export function executePostInitScript(

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

execFileSync(scriptPath, {stdio: 'inherit'});
return execa(scriptPath, {stdio: 'inherit'});
}
4 changes: 2 additions & 2 deletions packages/cli/src/commands/install/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@

import type {ContextT} from '../../tools/types.flow';
import logger from '../../tools/logger';
import * as PackageManager from '../../tools/PackageManager';
import * as PackageManager from '../../tools/packageManager';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you changed module name from PackageManager to packageManager, but the variable name stayed without changes. mind fixing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name left uppercased because it is a grouping of all methods in packageManager through * as importing.

import link from '../link/link';

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

logger.info(`Installing "${name}"...`);
PackageManager.install([name]);
await PackageManager.install([name]);

logger.info(`Linking "${name}"...`);
await link.func([name], ctx, {platforms: undefined});
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/install/uninstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import type {ContextT} from '../../tools/types.flow';
import logger from '../../tools/logger';
import * as PackageManager from '../../tools/PackageManager';
import * as PackageManager from '../../tools/packageManager';
import link from '../link/unlink';

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

logger.info(`Uninstalling "${name}"...`);
PackageManager.uninstall([name]);
await PackageManager.uninstall([name]);

logger.success(`Successfully uninstalled and unlinked "${name}"`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jest.mock(
() => ({name: 'TestApp', dependencies: {'react-native': '^0.57.8'}}),
{virtual: true},
);
jest.mock('../../../tools/PackageManager', () => ({
jest.mock('../../../tools/packageManager', () => ({
install: args => {
mockPushLog('$ yarn add', ...args);
},
Expand Down
Loading