Skip to content

Commit

Permalink
feat: support init with custom project directory (react-native-comm…
Browse files Browse the repository at this point in the history
…unity#365)

* Change `packageName` to `projectName` & add `projectPath` argument

* Add a way to specify a custom project path to init the app on

* Force the `init` command to create the project path recursively

* Add test for providing the project path on init

* Throw error if custom project path doesn’t exist and can’t be created

* Fix type error on `customProjectPath`

* Create directory before running the test with custom project path

* Use `mkdirp` to create project folder & Use `inquirer` to prompt for a directory replace

* Specify `directory` as an option

* Handle custom directory path after react logo

* Check if `ios` folder exists before trying to open it

* Update `init` tests

* Add `directory` type

* Remove unused `inquirer` from `init` test

* Only remove the project folder if it didn’t exist before running `init`

* Remove correct directory if it didn’t exist before `init`

* simplify

* make directory an argument instead of a flag

* update descriptions

* fix test

* use --directory after all

* address feedback; remove version handling from createFromTemplate

* flip the default
  • Loading branch information
lucasbento authored and dratwas committed Jul 12, 2019
1 parent 14ca6d8 commit d9a9c37
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 68 deletions.
58 changes: 38 additions & 20 deletions __e2e__/init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ test('init --template fails without package name', () => {
expect(stderr).toContain('missing required argument');
});

test('init --template', () => {
const templateFiles = [
'.buckconfig',
'.eslintrc.js',
'.flowconfig',
'.gitattributes',
// should be here, but it's not published yet
// '.gitignore',
'.watchmanconfig',
'App.js',
'__tests__',
'android',
'babel.config.js',
'index.js',
'ios',
'metro.config.js',
'node_modules',
'package.json',
'yarn.lock',
];
const templateFiles = [
'.buckconfig',
'.eslintrc.js',
'.flowconfig',
'.gitattributes',
// should be here, but it's not published yet
// '.gitignore',
'.watchmanconfig',
'App.js',
'__tests__',
'android',
'babel.config.js',
'index.js',
'ios',
'metro.config.js',
'node_modules',
'package.json',
'yarn.lock',
];

test('init --template', () => {
const {stdout} = run(DIR, [
'init',
'--template',
Expand Down Expand Up @@ -84,3 +84,21 @@ test('init --template file:/tmp/custom/template', () => {

expect(stdout).toContain('Run instructions');
});

test('init --template with custom project path', () => {
const projectName = 'TestInit';
const customPath = 'custom-path';

run(DIR, [
'init',
'--template',
'react-native-new-template',
projectName,
'--directory',
'custom-path',
]);

// make sure we don't leave garbage
expect(fs.readdirSync(DIR)).toEqual([customPath]);
expect(fs.readdirSync(path.join(DIR, customPath))).toEqual(templateFiles);
});
26 changes: 16 additions & 10 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,26 @@ Output project and dependencies configuration in JSON format to stdout. Used by

> Available since 0.60.0
Usage:
> **IMPORTANT**: Please note that this command is not available through `react-native-cli`, hence you need to either invoke it directly from `@react-native-community/cli` or `react-native` package which proxies binary to this CLI since 0.60.0, so it's possible to use it with e.g. `npx`.
Usage (with `npx`):

```sh
react-native init <projectName> [options]
npx react-native init <projectName> [options]
```

Initialize new React Native project. You can find out more use cases in [init docs](./init.md).
Initialize a new React Native project named <projectName> in a directory of the same name. You can find out more use cases in [init docs](./init.md).

#### Options

#### `--version [string]`

Uses a valid semver version of React Native as a template.

#### `--directory [string]`

Uses a custom directory instead of `<projectName>`.

#### `--template [string]`

Uses a custom template. Accepts following template sources:
Expand All @@ -140,10 +146,10 @@ Uses a custom template. Accepts following template sources:
Example:

```sh
react-native init MyApp --template react-native-custom-template
react-native init MyApp --template typescript
react-native init MyApp --template file:///Users/name/template-path
react-native init MyApp --template file:///Users/name/template-name-1.0.0.tgz
npx react-native init MyApp --template react-native-custom-template
npx react-native init MyApp --template typescript
npx react-native init MyApp --template file:///Users/name/template-path
npx react-native init MyApp --template file:///Users/name/template-name-1.0.0.tgz
```

A template is any directory or npm package that contains a `template.config.js` file in the root with following of the following type:
Expand All @@ -164,9 +170,9 @@ Example `template.config.js`:

```js
module.exports = {
placeholderName: "ProjectName",
templateDir: "./template",
postInitScript: "./script.js",
placeholderName: 'ProjectName',
templateDir: './template',
postInitScript: './script.js',
};
```

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/cliEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const handleError = err => {
// one modified to suit our needs
function printHelpInformation(examples, pkg) {
let cmdName = this._name;
const argsList = this._args
.map(arg => (arg.required ? `<${arg.name}>` : `[${arg.name}]`))
.join(' ');

if (this._alias) {
cmdName = `${cmdName}|${this._alias}`;
}
Expand All @@ -64,7 +68,7 @@ function printHelpInformation(examples, pkg) {
: [];

let output = [
chalk.bold(`react-native ${cmdName}`),
chalk.bold(`react-native ${cmdName} ${argsList}`),
this._description ? `\n${this._description}\n` : '',
...sourceInformation,
`${chalk.bold('Options:')}`,
Expand Down
16 changes: 11 additions & 5 deletions packages/cli/src/commands/init/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@ import init from './init';

export default {
func: init,
name: 'init <packageName>',
description: 'initialize new React Native project',
name: 'init <projectName>',
description:
'Initialize a new React Native project named <projectName> in a directory of the same name.',
options: [
{
name: '--version [string]',
description: 'Version of RN',
description: 'Uses a valid semver version of React Native as a template',
},
{
name: '--template [string]',
description: 'Custom template',
description:
'Uses a custom template. Valid arguments are: npm package, absolute directory prefixed with `file://`, Git repository or a tarball',
},
{
name: '--npm',
description: 'Force use of npm during initialization',
description: 'Forces using npm for initialization',
},
{
name: '--directory [string]',
description: 'Uses a custom directory instead of `<projectName>`.',
},
],
};
114 changes: 84 additions & 30 deletions packages/cli/src/commands/init/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import fs from 'fs-extra';
import Ora from 'ora';
import minimist from 'minimist';
import semver from 'semver';
import inquirer from 'inquirer';
import mkdirp from 'mkdirp';
import type {ConfigT} from 'types';
import {validateProjectName} from './validate';
import DirectoryAlreadyExistsError from './errors/DirectoryAlreadyExistsError';
Expand All @@ -22,12 +24,56 @@ import installPods from '../../tools/installPods';
import {processTemplateName} from './templateName';
import banner from './banner';
import {getLoader} from '../../tools/loader';
import {CLIError} from '@react-native-community/cli-tools';

const DEFAULT_VERSION = 'latest';

type Options = {|
template?: string,
npm?: boolean,
directory?: string,
|};

function doesDirectoryExist(dir: string) {
return fs.existsSync(dir);
}

function getProjectDirectory({projectName, directory}): string {
return path.relative(process.cwd(), directory || projectName);
}

async function setProjectDirectory(directory) {
const directoryExists = doesDirectoryExist(directory);
if (directoryExists) {
const {shouldReplaceprojectDirectory} = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldReplaceprojectDirectory',
message: `Directory "${directory}" already exists, do you want to replace it?`,
default: false,
},
]);

if (!shouldReplaceprojectDirectory) {
throw new DirectoryAlreadyExistsError(directory);
}

await fs.emptyDir(directory);
}

try {
mkdirp.sync(directory);
process.chdir(directory);
} catch (error) {
throw new CLIError(
`Error occurred while trying to ${
directoryExists ? 'replace' : 'create'
} project directory.`,
error,
);
}
}

function adjustNameIfUrl(name, cwd) {
// We use package manager to infer the name of the template module for us.
// That's why we get it from temporary package.json, where the name is the
Expand All @@ -44,33 +90,28 @@ function adjustNameIfUrl(name, cwd) {
async function createFromTemplate({
projectName,
templateName,
version,
npm,
directory,
}: {
projectName: string,
templateName: string,
version?: string,
npm?: boolean,
directory: string,
}) {
logger.debug('Initializing new project');
logger.log(banner);

await setProjectDirectory(directory);

const Loader = getLoader();
const loader = new Loader({text: 'Downloading template'});
const templateSourceDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'rncli-init-template-'),
);

if (version && semver.valid(version) && !semver.gte(version, '0.60.0-rc.0')) {
throw new Error(
'Cannot use React Native CLI to initialize project with version lower than 0.60.0.',
);
}

try {
loader.start();
let {uri, name} = await processTemplateName(
version ? `${templateName}@${version}` : templateName,
);
let {uri, name} = await processTemplateName(templateName);

await installTemplatePackage(uri, templateSourceDir, npm);

Expand Down Expand Up @@ -127,49 +168,62 @@ async function installDependencies({
loader.succeed();
}

function createProject(projectName: string, options: Options, version: string) {
fs.mkdirSync(projectName);
process.chdir(projectName);
async function createProject(
projectName: string,
directory: string,
version: string,
options: Options,
) {
const templateName = options.template || `react-native@${version}`;

if (options.template) {
return createFromTemplate({
projectName,
templateName: options.template,
npm: options.npm,
});
if (
version !== DEFAULT_VERSION &&
semver.valid(version) &&
!semver.gte(version, '0.60.0-rc.0')
) {
throw new Error(
'Cannot use React Native CLI to initialize project with version lower than 0.60.0.',
);
}

return createFromTemplate({
projectName,
templateName: 'react-native',
version,
templateName,
npm: options.npm,
directory,
});
}

export default (async function initialize(
[projectName]: Array<string>,
_context: ConfigT,
context: ConfigT,
options: Options,
) {
const rootFolder = context.root;

validateProjectName(projectName);

/**
* Commander is stripping `version` from options automatically.
* We have to use `minimist` to take that directly from `process.argv`
*/
const version: string = minimist(process.argv).version || 'latest';
const version: string = minimist(process.argv).version || DEFAULT_VERSION;

if (fs.existsSync(projectName)) {
throw new DirectoryAlreadyExistsError(projectName);
}
const directoryName = getProjectDirectory({
projectName,
directory: options.directory || projectName,
});
const directoryExists = doesDirectoryExist(directoryName);

try {
await createProject(projectName, options, version);
await createProject(projectName, directoryName, version, options);

printRunInstructions(process.cwd(), projectName);
printRunInstructions(rootFolder, projectName);
} catch (e) {
logger.error(e.message);
fs.removeSync(projectName);
// Only remove project if it didn't exist before running `init`
if (!directoryExists) {
fs.removeSync(path.resolve(rootFolder, directoryName));
}
}
});
8 changes: 6 additions & 2 deletions packages/cli/src/tools/installPods.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import fs from 'fs-extra';
import fs from 'fs';
import execa from 'execa';
import chalk from 'chalk';
import Ora from 'ora';
Expand All @@ -17,9 +17,13 @@ async function installPods({
loader?: typeof Ora,
}) {
try {
if (!fs.existsSync('ios')) {
return;
}

process.chdir('ios');

const hasPods = await fs.pathExists('Podfile');
const hasPods = fs.existsSync('Podfile');

if (!hasPods) {
return;
Expand Down

0 comments on commit d9a9c37

Please sign in to comment.