Skip to content

Commit a7c12e3

Browse files
committed
feat: add abilty to generate a react-native project
1 parent 6b37530 commit a7c12e3

File tree

76 files changed

+3002
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+3002
-5
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
coverage/
33
lib/
4+
templates/

README.md

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
# @react-native-community/bob
22

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

55
## Features
66

7-
The CLI can build code for following targets:
7+
### Scaffold new projects
8+
9+
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:
10+
11+
- Simple example modules for Android and iOS which you can build upon
12+
- [Kotlin](https://kotlinlang.org/) configured for building the module for Android
13+
- Example React Native app to manually test your modules
14+
- [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) and [Husky](https://github.com/typicode/husky) pre-configured
15+
- Bob pre-configured to compile your files
16+
- CircleCI pre-configured to run tests on the CI
17+
18+
<img src="assets/bob-create.gif" width="480px" height="auto">
19+
20+
### Build your projects
21+
22+
Bob can build code for following targets:
823

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

15-
## Why?
30+
## Why
1631

1732
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.
1833

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

3146
## Usage
3247

48+
### Creating a new project
49+
50+
To create new project with Bob, run the following:
51+
52+
```sh
53+
npx @react-native-community/bob create react-native-awesome-module
54+
```
55+
56+
This will ask you few questions about your project and generate a new project in a folder named `react-native-awesome-module`.
57+
58+
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.
59+
60+
### Configuring an existing project
61+
3362
To configure your project to use Bob, open a Terminal and run `yarn bob init` for automatic configuration.
3463

3564
To configure your project manually, follow these steps:
@@ -162,6 +191,13 @@ Example:
162191
["aar", { "reverseJetify": true }]
163192
```
164193

194+
## Acknowledgements
195+
196+
Thanks to the authors of these libraries for inspiration:
197+
198+
- [create-react-native-module](https://github.com/brodybits/create-react-native-module)
199+
- [react-native-webview](https://github.com/react-native-community/react-native-webview)
200+
165201
## LICENSE
166202

167203
MIT

assets/bob-create.gif

502 KB
Loading

package.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
},
1212
"files": [
1313
"bin",
14-
"lib"
14+
"lib",
15+
"templates"
1516
],
1617
"publishConfig": {
1718
"access": "public",
@@ -34,12 +35,16 @@
3435
"@babel/preset-typescript": "^7.8.3",
3536
"chalk": "^3.0.0",
3637
"cosmiconfig": "^6.0.0",
38+
"dedent": "^0.7.0",
3739
"del": "^5.1.0",
40+
"ejs": "^3.0.1",
3841
"fs-extra": "^8.1.0",
42+
"github-username": "^5.0.1",
3943
"glob": "^7.1.6",
4044
"inquirer": "^7.0.4",
4145
"is-git-dirty": "^1.0.0",
4246
"json5": "^2.1.1",
47+
"validate-npm-package-name": "^3.0.0",
4348
"yargs": "^15.1.0"
4449
},
4550
"optionalDependencies": {
@@ -51,11 +56,14 @@
5156
"@release-it/conventional-changelog": "^1.1.0",
5257
"@types/babel__core": "^7.1.3",
5358
"@types/chalk": "^2.2.0",
59+
"@types/dedent": "^0.7.0",
5460
"@types/del": "^4.0.0",
61+
"@types/ejs": "^3.0.0",
5562
"@types/fs-extra": "^8.0.1",
5663
"@types/glob": "^7.1.1",
5764
"@types/inquirer": "^6.5.0",
5865
"@types/json5": "^0.0.30",
66+
"@types/validate-npm-package-name": "^3.0.0",
5967
"@types/yargs": "^15.0.3",
6068
"commitlint": "^8.3.5",
6169
"eslint": "^6.8.0",

src/cli.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import yargs from 'yargs';
55
import inquirer from 'inquirer';
66
import { cosmiconfigSync } from 'cosmiconfig';
77
import isGitDirty from 'is-git-dirty';
8+
import create from './create';
89
import * as logger from './utils/logger';
910
import buildAAR from './targets/aar';
1011
import buildCommonJS from './targets/commonjs';
@@ -24,6 +25,7 @@ const FLOW_PRGAMA_REGEX = /\*?\s*@(flow)\b/m;
2425

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

src/create.ts

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import child_process from 'child_process';
2+
import path from 'path';
3+
import fs from 'fs-extra';
4+
import ejs from 'ejs';
5+
import dedent from 'dedent';
6+
import chalk from 'chalk';
7+
import inquirer from 'inquirer';
8+
import yargs from 'yargs';
9+
import validateNpmPackage from 'validate-npm-package-name';
10+
import githubUsername from 'github-username';
11+
12+
const TEMPLATE = path.resolve(__dirname, '../templates/library');
13+
const BINARIES = /(gradlew|\.(jar|xib|keystore|png|jpg|gif))$/;
14+
15+
export default async function create(argv: yargs.Arguments<any>) {
16+
const folder = path.join(process.cwd(), argv.name);
17+
18+
if (await fs.pathExists(folder)) {
19+
console.log(
20+
`A folder already exists at ${chalk.blue(
21+
folder
22+
)}! Please specify another folder name or delete the existing one.`
23+
);
24+
25+
process.exit(1);
26+
}
27+
28+
let name, email;
29+
30+
try {
31+
name = child_process
32+
.execSync('git config --get user.name')
33+
.toString()
34+
.trim();
35+
36+
email = child_process
37+
.execSync('git config --get user.email')
38+
.toString()
39+
.trim();
40+
} catch (e) {
41+
// Ignore error
42+
}
43+
44+
const {
45+
slug,
46+
description,
47+
authorName,
48+
authorEmail,
49+
authorUrl,
50+
githubUrl: repo,
51+
} = (await inquirer.prompt([
52+
{
53+
type: 'input',
54+
name: 'slug',
55+
message: "What is the name of the package? (e.g. 'react-native-magic')",
56+
default: path.basename(argv.name),
57+
validate: input => validateNpmPackage(input).validForNewPackages,
58+
},
59+
{
60+
type: 'input',
61+
name: 'description',
62+
message: 'What is the description for the package?',
63+
validate: input => Boolean(input),
64+
},
65+
{
66+
type: 'input',
67+
name: 'authorName',
68+
message: 'What is the name of package author?',
69+
default: name,
70+
validate: input => Boolean(input),
71+
},
72+
{
73+
type: 'input',
74+
name: 'authorEmail',
75+
message: 'What is the email address for the package author?',
76+
default: email,
77+
validate: input => input.includes('@'),
78+
},
79+
{
80+
type: 'input',
81+
name: 'authorUrl',
82+
message: 'What is the URL for the package author?',
83+
default: async (answers: any) => {
84+
try {
85+
const username = await githubUsername(answers.authorEmail);
86+
87+
return `https://github.com/${username}`;
88+
} catch (e) {
89+
// Ignore error
90+
}
91+
92+
return undefined;
93+
},
94+
validate: input => /^https?:\/\//.test(input),
95+
},
96+
{
97+
type: 'input',
98+
name: 'githubUrl',
99+
message: 'What is the URL for the repository?',
100+
default: (answers: any) => {
101+
if (/^https?:\/\/github.com\/[^/]+/.test(answers.authorUrl)) {
102+
return `${answers.authorUrl}/${answers.slug
103+
.replace(/^@/, '')
104+
.replace(/\//g, '-')}`;
105+
}
106+
107+
return undefined;
108+
},
109+
validate: input => /^https?:\/\//.test(input),
110+
},
111+
])) as {
112+
slug: string;
113+
description: string;
114+
authorName: string;
115+
authorEmail: string;
116+
authorUrl: string;
117+
githubUrl: string;
118+
};
119+
120+
const project = slug.replace(/^(react-native-|@[^/]+\/)/, '');
121+
122+
const options = {
123+
project: {
124+
slug,
125+
description,
126+
name: `${project
127+
.charAt(0)
128+
.toUpperCase()}${project
129+
.replace(/[^a-z0-9](\w)/g, (_, $1) => $1.toUpperCase())
130+
.slice(1)}`,
131+
package: project.replace(/[^a-z0-9]/g, '').toLowerCase(),
132+
},
133+
author: {
134+
name: authorName,
135+
email: authorEmail,
136+
url: authorUrl,
137+
},
138+
repo,
139+
};
140+
141+
const copyDir = async (source: string, dest: string) => {
142+
await fs.mkdirp(dest);
143+
144+
const files = await fs.readdir(source);
145+
146+
for (const f of files) {
147+
const target = path.join(
148+
dest,
149+
ejs.render(f.startsWith('__') ? f : f.replace(/^_/, '.'), options)
150+
);
151+
152+
const file = path.join(source, f);
153+
const stats = await fs.stat(file);
154+
155+
if (stats.isDirectory()) {
156+
await copyDir(file, target);
157+
} else if (!file.match(BINARIES)) {
158+
const content = await fs.readFile(file, 'utf8');
159+
160+
await fs.writeFile(target, ejs.render(content, options));
161+
} else {
162+
await fs.copyFile(file, target);
163+
}
164+
}
165+
};
166+
167+
await copyDir(TEMPLATE, folder);
168+
169+
console.log(
170+
dedent(chalk`
171+
Project created successfully at {blue ${argv.name}}!
172+
173+
{magenta {bold Get started} with the project}{gray :}
174+
175+
{gray $} yarn bootstrap
176+
177+
{cyan Run the example app on {bold iOS}}{gray :}
178+
179+
{gray $} yarn example ios
180+
181+
{green Run the example app on {bold Android}}{gray :}
182+
183+
{gray $} yarn example android
184+
185+
{blue Good luck!}
186+
`)
187+
);
188+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require "json"
2+
3+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4+
5+
Pod::Spec.new do |s|
6+
s.name = "<%= project.package %>"
7+
s.version = package["version"]
8+
s.summary = package["description"]
9+
s.homepage = package["homepage"]
10+
s.license = package["license"]
11+
s.authors = package["author"]
12+
13+
s.platforms = { :ios => "9.0" }
14+
s.source = { :git => "<%= repo %>", :tag => "#{s.version}" }
15+
16+
s.source_files = "ios/**/*.{h,m}"
17+
18+
s.dependency "React"
19+
end

0 commit comments

Comments
 (0)