Skip to content
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

feat: support init with custom project directory #365

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
42db018
Change `packageName` to `projectName` & add `projectPath` argument
lucasbento May 1, 2019
9e59f68
Add a way to specify a custom project path to init the app on
lucasbento May 1, 2019
6921e2d
Force the `init` command to create the project path recursively
lucasbento May 1, 2019
34863ae
Add test for providing the project path on init
lucasbento May 1, 2019
e990db6
Throw error if custom project path doesn’t exist and can’t be created
lucasbento May 2, 2019
39a3671
Fix type error on `customProjectPath`
lucasbento May 2, 2019
c12390c
Create directory before running the test with custom project path
lucasbento May 2, 2019
ed66455
Use `mkdirp` to create project folder & Use `inquirer` to prompt for …
lucasbento May 12, 2019
c18f173
Specify `directory` as an option
lucasbento May 16, 2019
2b3affe
Handle custom directory path after react logo
lucasbento May 16, 2019
34fe775
Check if `ios` folder exists before trying to open it
lucasbento May 16, 2019
b0b2200
Update `init` tests
lucasbento May 16, 2019
0fa4364
Add `directory` type
lucasbento May 16, 2019
e661454
Remove unused `inquirer` from `init` test
lucasbento May 16, 2019
6bfc2e6
Only remove the project folder if it didn’t exist before running `init`
lucasbento May 16, 2019
579862e
Remove correct directory if it didn’t exist before `init`
lucasbento May 17, 2019
f9f9cc5
simplify
thymikee May 17, 2019
408d55f
make directory an argument instead of a flag
thymikee May 17, 2019
a68a92b
update descriptions
thymikee May 17, 2019
953e768
fix test
thymikee May 17, 2019
dc3aefb
use --directory after all
thymikee May 17, 2019
d3bd052
address feedback; remove version handling from createFromTemplate
thymikee May 17, 2019
6366107
flip the default
thymikee May 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
23 changes: 13 additions & 10 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ 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> [directory] [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 or specified by an optional `[directory]` argument. You can find out more use cases in [init docs](./init.md).

#### Options

Expand All @@ -140,10 +142,11 @@ 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 CustomDirectory
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 +167,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
10 changes: 6 additions & 4 deletions packages/cli/src/commands/init/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import init from './init';

export default {
func: init,
name: 'init <packageName>',
description: 'initialize new React Native project',
name: 'init <projectName> [directory]',
description:
'Initialize a new React Native project named <projectName> in a directory of the same name or specified by an optional [directory] argument.',
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',
Expand Down
93 changes: 73 additions & 20 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,52 @@ 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';

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

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([
thymikee marked this conversation as resolved.
Show resolved Hide resolved
{
type: 'confirm',
name: 'shouldReplaceprojectDirectory',
message: `Directory "${directory}" already exists, do you want to replace it?`,
},
]);

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 @@ -46,14 +88,19 @@ async function createFromTemplate({
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(
Expand Down Expand Up @@ -127,31 +174,32 @@ async function installDependencies({
loader.succeed();
}

function createProject(projectName: string, options: Options, version: string) {
fs.mkdirSync(projectName);
process.chdir(projectName);

if (options.template) {
return createFromTemplate({
projectName,
templateName: options.template,
npm: options.npm,
});
}
async function createProject(
projectName: string,
directory: string,
version: string,
options: Options,
) {
const templateName = options.template || 'react-native';

return createFromTemplate({
projectName,
templateName: 'react-native',
version,
templateName,
// version is "latest" by default, but it's easier for us to treat it as
// undefined when the "template" param is passed. Might refactor later
version: options.template ? undefined : version,
thymikee marked this conversation as resolved.
Show resolved Hide resolved
npm: options.npm,
directory,
});
}

export default (async function initialize(
[projectName]: Array<string>,
[projectName, directory]: Array<string>,
_context: ConfigT,
options: Options,
) {
const rootFolder = process.cwd();
thymikee marked this conversation as resolved.
Show resolved Hide resolved

validateProjectName(projectName);

/**
Expand All @@ -160,16 +208,21 @@ export default (async function initialize(
*/
const version: string = minimist(process.argv).version || 'latest';

if (fs.existsSync(projectName)) {
throw new DirectoryAlreadyExistsError(projectName);
}
const directoryName = getProjectDirectory({
projectName,
directory: 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));
}
}
});
4 changes: 4 additions & 0 deletions packages/cli/src/tools/installPods.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ async function installPods({
loader?: typeof Ora,
}) {
try {
if (!(await fs.pathExists('ios'))) {
thymikee marked this conversation as resolved.
Show resolved Hide resolved
return;
}

process.chdir('ios');

const hasPods = await fs.pathExists('Podfile');
thymikee marked this conversation as resolved.
Show resolved Hide resolved
Expand Down