Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: add abilty to generate a react-native project
  • Loading branch information
satya164 committed Feb 9, 2020
commit 0830cdca324038df16998ea2040230dd7231474d
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
coverage/
lib/
templates/
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
# @react-native-community/bob

👷‍♂️ Simple CLI to build React Native libraries for different targets.
👷‍♂️ Simple CLI to scaffold and build React Native libraries for different targets.

## Features

The CLI can build code for following targets:
### Scaffold new projects

If you want to create your own React Native module, scaffolding the project can be a daunting task. Bob can scaffold a new project for you with the following things:

- Simple example modules for Android and iOS which you can build upon
- [Kotlin](https://kotlinlang.org/) configured for building the module for Android
- Example React Native app to manually test your modules
- [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) and [Husky](https://github.com/typicode/husky) pre-configured
- Bob pre-configured to compile your files
- CircleCI pre-configured to run tests on the CI

<img src="assets/bob-create.gif" width="480px" height="auto">

### Build your projects

Bob can build code for following targets:

- Generic CommonJS build
- ES modules build for bundlers such as webpack
- Flow definitions (copies .js files to .flow files)
- TypeScript definitions (uses `tsc` to generate declaration files)
- Android AAR files

## Why?
## Why

Metro handles compiling source code for React Native libraries, but it's possible to use them in other targets such as web. Currently, to handle this, we need to have multiple babel configs and write a long `babel-cli` command in our `package.json`. We also need to keep the configs in sync between our projects.

Expand All @@ -30,6 +45,20 @@ yarn add --dev @react-native-community/bob

## Usage

### Creating a new project

To create new project with Bob, run the following:

```sh
npx @react-native-community/bob create react-native-awesome-module
```

This will ask you few questions about your project and generate a new project in a folder named `react-native-awesome-module`.

The difference from [create-react-native-module](https://github.com/brodybits/create-react-native-module) is that the generated project with Bob is very opinionated and configured with additional tools.

### Configuring an existing project

To configure your project to use Bob, open a Terminal and run `yarn bob init` for automatic configuration.

To configure your project manually, follow these steps:
Expand Down Expand Up @@ -162,6 +191,13 @@ Example:
["aar", { "reverseJetify": true }]
```

## Acknowledgements

Thanks to the authors of these libraries for inspiration:

- [create-react-native-module](https://github.com/brodybits/create-react-native-module)
- [react-native-webview](https://github.com/react-native-community/react-native-webview)

## LICENSE

MIT
Binary file added assets/bob-create.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@react-native-community/bob",
"version": "0.8.0",
"version": "0.9.0",
"description": "CLI to build JavaScript files for React Native libraries",
"repository": "git@github.com:react-native-community/bob.git",
"author": "Satyajit Sahoo <satyajit.happy@gmail.com>",
Expand All @@ -11,11 +11,12 @@
},
"files": [
"bin",
"lib"
"lib",
"templates"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
"registry": "http://localhost:4873/"
},
"scripts": {
"lint": "eslint --ext '.js,.ts,.tsx' .",
Expand All @@ -34,12 +35,16 @@
"@babel/preset-typescript": "^7.8.3",
"chalk": "^3.0.0",
"cosmiconfig": "^6.0.0",
"dedent": "^0.7.0",
"del": "^5.1.0",
"ejs": "^3.0.1",
"fs-extra": "^8.1.0",
"github-username": "^5.0.1",
"glob": "^7.1.6",
"inquirer": "^7.0.4",
"is-git-dirty": "^1.0.0",
"json5": "^2.1.1",
"validate-npm-package-name": "^3.0.0",
"yargs": "^15.1.0"
},
"optionalDependencies": {
Expand All @@ -51,11 +56,14 @@
"@release-it/conventional-changelog": "^1.1.0",
"@types/babel__core": "^7.1.3",
"@types/chalk": "^2.2.0",
"@types/dedent": "^0.7.0",
"@types/del": "^4.0.0",
"@types/ejs": "^3.0.0",
"@types/fs-extra": "^8.0.1",
"@types/glob": "^7.1.1",
"@types/inquirer": "^6.5.0",
"@types/json5": "^0.0.30",
"@types/validate-npm-package-name": "^3.0.0",
"@types/yargs": "^15.0.3",
"commitlint": "^8.3.5",
"eslint": "^6.8.0",
Expand Down
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import yargs from 'yargs';
import inquirer from 'inquirer';
import { cosmiconfigSync } from 'cosmiconfig';
import isGitDirty from 'is-git-dirty';
import create from './create';
import * as logger from './utils/logger';
import buildAAR from './targets/aar';
import buildCommonJS from './targets/commonjs';
Expand All @@ -24,6 +25,7 @@ const FLOW_PRGAMA_REGEX = /\*?\s*@(flow)\b/m;

// eslint-disable-next-line babel/no-unused-expressions
yargs
.command('create <name>', 'create a react native library', {}, create)
.command('init', 'configure the package to use bob', {}, async () => {
const pak = path.join(root, 'package.json');

Expand Down
188 changes: 188 additions & 0 deletions src/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import child_process from 'child_process';
import path from 'path';
import fs from 'fs-extra';
import ejs from 'ejs';
import dedent from 'dedent';
import chalk from 'chalk';
import inquirer from 'inquirer';
import yargs from 'yargs';
import validateNpmPackage from 'validate-npm-package-name';
import githubUsername from 'github-username';

const TEMPLATE = path.resolve(__dirname, '../templates/library');
const BINARIES = /(gradlew|\.(jar|xib|keystore|png|jpg|gif))$/;

export default async function create(argv: yargs.Arguments<any>) {
const folder = path.join(process.cwd(), argv.name);

if (await fs.pathExists(folder)) {
console.log(
`A folder already exists at ${chalk.blue(
folder
)}! Please specify another folder name or delete the existing one.`
);

process.exit(1);
}

let name, email;

try {
name = child_process
.execSync('git config --get user.name')
.toString()
.trim();

email = child_process
.execSync('git config --get user.email')
.toString()
.trim();
} catch (e) {
// Ignore error
}

const {
slug,
description,
authorName,
authorEmail,
authorUrl,
githubUrl: repo,
} = (await inquirer.prompt([
{
type: 'input',
name: 'slug',
message: "What is the name of the package? (e.g. 'react-native-magic')",
default: path.basename(argv.name),
validate: input => validateNpmPackage(input).validForNewPackages,
},
{
type: 'input',
name: 'description',
message: 'What is the description for the package?',
validate: input => Boolean(input),
},
{
type: 'input',
name: 'authorName',
message: 'What is the name of package author?',
default: name,
validate: input => Boolean(input),
},
{
type: 'input',
name: 'authorEmail',
message: 'What is the email address for the package author?',
default: email,
validate: input => input.includes('@'),
},
{
type: 'input',
name: 'authorUrl',
message: 'What is the URL for the package author?',
default: async (answers: any) => {
try {
const username = await githubUsername(answers.authorEmail);

return `https://github.com/${username}`;
} catch (e) {
// Ignore error
}

return undefined;
},
validate: input => /^https?:\/\//.test(input),
},
{
type: 'input',
name: 'githubUrl',
message: 'What is the URL for the repository?',
default: (answers: any) => {
if (/^https?:\/\/github.com\/[^/]+/.test(answers.authorUrl)) {
return `${answers.authorUrl}/${answers.slug
.replace(/^@/, '')
.replace(/\//g, '-')}`;
}

return undefined;
},
validate: input => /^https?:\/\//.test(input),
},
])) as {
slug: string;
description: string;
authorName: string;
authorEmail: string;
authorUrl: string;
githubUrl: string;
};

const project = slug.replace(/^(react-native-|@[^/]+\/)/, '');

const options = {
project: {
slug,
description,
name: `${project
.charAt(0)
.toUpperCase()}${project
.replace(/[^a-z0-9](\w)/g, (_, $1) => $1.toUpperCase())
.slice(1)}`,
package: project.replace(/[^a-z0-9]/g, '').toLowerCase(),
},
author: {
name: authorName,
email: authorEmail,
url: authorUrl,
},
repo,
};

const copyDir = async (source: string, dest: string) => {
await fs.mkdirp(dest);

const files = await fs.readdir(source);

for (const f of files) {
const target = path.join(
dest,
ejs.render(f.startsWith('__') ? f : f.replace(/^_/, '.'), options)
);

const file = path.join(source, f);
const stats = await fs.stat(file);

if (stats.isDirectory()) {
await copyDir(file, target);
} else if (!file.match(BINARIES)) {
const content = await fs.readFile(file, 'utf8');

await fs.writeFile(target, ejs.render(content, options));
} else {
await fs.copyFile(file, target);
}
}
};

await copyDir(TEMPLATE, folder);

console.log(
dedent(chalk`
Project created successfully at {blue ${argv.name}}!

{magenta {bold Get started} with the project}{gray :}

{gray $} yarn bootstrap

{cyan Run the example app on {bold iOS}}{gray :}

{gray $} yarn example ios

{green Run the example app on {bold Android}}{gray :}

{gray $} yarn example android

{blue Good luck!}
`)
);
}
19 changes: 19 additions & 0 deletions templates/library/<%= project.package %>.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
s.name = "<%= project.package %>"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]

s.platforms = { :ios => "9.0" }
s.source = { :git => "<%= repo %>", :tag => "#{s.version}" }

s.source_files = "ios/**/*.{h,m}"

s.dependency "React"
end
Loading