@bug_hunter/expo-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +2 -0
- package/package.json +34 -0
- package/src/index.js +39 -0
- package/src/prompts.js +44 -0
- package/src/steps/clone.js +36 -0
- package/src/steps/configure.js +34 -0
- package/src/steps/git.js +34 -0
- package/src/steps/install.js +19 -0
- package/src/steps/modules.js +76 -0
- package/src/utils/logger.js +38 -0
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bug_hunter/expo-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to scaffold a React Native + Expo project from your base template",
|
|
5
|
+
"bin": {
|
|
6
|
+
"expo-kit": "./bin/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node src/index.js",
|
|
10
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
11
|
+
},
|
|
12
|
+
"author": "bug_hunter",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react-native",
|
|
15
|
+
"expo",
|
|
16
|
+
"cli",
|
|
17
|
+
"boilerplate",
|
|
18
|
+
"starter",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/zartabkhalil/expo-kit.git"
|
|
25
|
+
},
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^4.1.2",
|
|
29
|
+
"enquirer": "^2.4.1",
|
|
30
|
+
"execa": "^5.1.1",
|
|
31
|
+
"fs-extra": "^11.3.5",
|
|
32
|
+
"ora": "^5.4.1"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { askQuestions } = require("./prompts");
|
|
3
|
+
const { cloneTemplate } = require("./steps/clone");
|
|
4
|
+
const { configureApp } = require("./steps/configure");
|
|
5
|
+
const { resetGit } = require("./steps/git");
|
|
6
|
+
const { configureModules } = require("./steps/modules");
|
|
7
|
+
const { installDependencies } = require("./steps/install");
|
|
8
|
+
const logger = require("./utils/logger");
|
|
9
|
+
|
|
10
|
+
async function run() {
|
|
11
|
+
// ─── Welcome ───────────────────────────────────────────────
|
|
12
|
+
logger.welcome();
|
|
13
|
+
|
|
14
|
+
// ─── Step 1: Ask questions ─────────────────────────────────
|
|
15
|
+
const answers = await askQuestions();
|
|
16
|
+
|
|
17
|
+
// ─── Compute target path ───────────────────────────────────
|
|
18
|
+
const targetPath = path.join(process.cwd(), answers.projectName);
|
|
19
|
+
|
|
20
|
+
// ─── Step 2: Clone template ────────────────────────────────
|
|
21
|
+
await cloneTemplate(answers, targetPath);
|
|
22
|
+
|
|
23
|
+
// ─── Step 3: Configure app.json ───────────────────────────
|
|
24
|
+
await configureApp(answers, targetPath);
|
|
25
|
+
|
|
26
|
+
// ─── Step 4: Reset git history ─────────────────────────────
|
|
27
|
+
await resetGit(answers, targetPath);
|
|
28
|
+
|
|
29
|
+
// ─── Step 5: Configure modules ─────────────────────────────
|
|
30
|
+
await configureModules(answers, targetPath);
|
|
31
|
+
|
|
32
|
+
// ─── Step 6: Install dependencies ─────────────────────────
|
|
33
|
+
await installDependencies(answers, targetPath);
|
|
34
|
+
|
|
35
|
+
// ─── Done ──────────────────────────────────────────────────
|
|
36
|
+
logger.success(answers);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
run();
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { prompt } = require("enquirer");
|
|
2
|
+
|
|
3
|
+
async function askQuestions() {
|
|
4
|
+
const answers = await prompt([
|
|
5
|
+
{
|
|
6
|
+
type: "input",
|
|
7
|
+
name: "projectName",
|
|
8
|
+
message: "Project name?",
|
|
9
|
+
initial: "my-app",
|
|
10
|
+
validate(value) {
|
|
11
|
+
if (!value) return "Project name is required";
|
|
12
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
13
|
+
return "Only lowercase letters, numbers and hyphens";
|
|
14
|
+
return true;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: "input",
|
|
19
|
+
name: "bundleId",
|
|
20
|
+
message: "Bundle ID?",
|
|
21
|
+
initial: "com.yourname.myapp",
|
|
22
|
+
validate(value) {
|
|
23
|
+
if (!value) return "Bundle ID is required";
|
|
24
|
+
if (!/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(value))
|
|
25
|
+
return "Must be like com.yourname.appname";
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "multiselect",
|
|
31
|
+
name: "selectedModules",
|
|
32
|
+
message: "Which modules do you need?",
|
|
33
|
+
choices: [
|
|
34
|
+
{ name: "auth", message: "Auth — email + password login" },
|
|
35
|
+
{ name: "redux", message: "Redux — state management" },
|
|
36
|
+
],
|
|
37
|
+
hint: "(Space to select, Enter to confirm)",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
return answers;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { askQuestions };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const ora = require("ora");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const execa = require("execa");
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const logger = require("../utils/logger");
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_REPO = "https://github.com/zartabkhalil/rn-template.git";
|
|
9
|
+
|
|
10
|
+
async function cloneTemplate(answers) {
|
|
11
|
+
const { projectName } = answers;
|
|
12
|
+
const targetPath = path.join(process.cwd(), projectName);
|
|
13
|
+
|
|
14
|
+
// Check folder doesn't already exist
|
|
15
|
+
if (await fs.pathExists(targetPath)) {
|
|
16
|
+
logger.error(
|
|
17
|
+
`Folder "${projectName}" already exists. Choose a different name.`,
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const spinner = ora(`Cloning template into ${projectName}...`).start();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await execa("git", ["clone", TEMPLATE_REPO, projectName]);
|
|
26
|
+
spinner.succeed(chalk.green("Template cloned"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
spinner.fail(chalk.red("Failed to clone template"));
|
|
29
|
+
logger.error(error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return targetPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { cloneTemplate, TEMPLATE_REPO };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const ora = require("ora");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const logger = require("../utils/logger");
|
|
6
|
+
|
|
7
|
+
async function configureApp(answers, targetPath) {
|
|
8
|
+
const { projectName, bundleId } = answers;
|
|
9
|
+
|
|
10
|
+
const spinner = ora("Updating app name and bundle ID...").start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const appJsonPath = path.join(targetPath, "app.json");
|
|
14
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
15
|
+
|
|
16
|
+
// Update app name
|
|
17
|
+
appJson.expo.name = projectName;
|
|
18
|
+
appJson.expo.slug = projectName;
|
|
19
|
+
appJson.expo.scheme = projectName;
|
|
20
|
+
|
|
21
|
+
// Update bundle ID
|
|
22
|
+
appJson.expo.ios.bundleIdentifier = bundleId;
|
|
23
|
+
appJson.expo.android.package = bundleId;
|
|
24
|
+
|
|
25
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
26
|
+
spinner.succeed(chalk.green("app.json updated"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
spinner.fail(chalk.red("Failed to update app.json"));
|
|
29
|
+
logger.error(error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { configureApp };
|
package/src/steps/git.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const ora = require("ora");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const execa = require("execa");
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const logger = require("../utils/logger");
|
|
7
|
+
|
|
8
|
+
async function resetGit(answers, targetPath) {
|
|
9
|
+
const { projectName } = answers;
|
|
10
|
+
|
|
11
|
+
const spinner = ora("Resetting git history...").start();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Remove existing git history from template
|
|
15
|
+
await fs.remove(path.join(targetPath, ".git"));
|
|
16
|
+
|
|
17
|
+
// Init a fresh repo
|
|
18
|
+
await execa("git", ["init"], { cwd: targetPath });
|
|
19
|
+
await execa("git", ["add", "."], { cwd: targetPath });
|
|
20
|
+
await execa(
|
|
21
|
+
"git",
|
|
22
|
+
["commit", "-m", `init: ${projectName} created with create-rn-app`],
|
|
23
|
+
{ cwd: targetPath },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
spinner.succeed(chalk.green("Git history reset"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
spinner.fail(chalk.red("Failed to reset git history"));
|
|
29
|
+
logger.error(error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { resetGit };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const ora = require("ora");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const execa = require("execa");
|
|
4
|
+
const logger = require("../utils/logger");
|
|
5
|
+
|
|
6
|
+
async function installDependencies(answers, targetPath) {
|
|
7
|
+
const spinner = ora("Installing dependencies...").start();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await execa("npm", ["install", "--legacy-peer-deps"], { cwd: targetPath });
|
|
11
|
+
spinner.succeed(chalk.green("Dependencies installed"));
|
|
12
|
+
} catch (error) {
|
|
13
|
+
spinner.fail(chalk.red("Failed to install dependencies"));
|
|
14
|
+
logger.error(error.message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { installDependencies };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const ora = require("ora");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const logger = require("../utils/logger");
|
|
6
|
+
|
|
7
|
+
// ─── Module definitions ────────────────────────────────────────
|
|
8
|
+
// Add new modules here as the toolkit grows
|
|
9
|
+
const MODULE_REGISTRY = {
|
|
10
|
+
auth: {
|
|
11
|
+
folder: "auth",
|
|
12
|
+
deps: [
|
|
13
|
+
"expo-secure-store",
|
|
14
|
+
"react-hook-form",
|
|
15
|
+
"@hookform/resolvers",
|
|
16
|
+
"zod",
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
redux: {
|
|
20
|
+
folder: "redux",
|
|
21
|
+
deps: ["@reduxjs/toolkit", "react-redux"],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function configureModules(answers, targetPath) {
|
|
26
|
+
const { selectedModules } = answers;
|
|
27
|
+
const modulesPath = path.join(targetPath, "src", "modules");
|
|
28
|
+
const packageJsonPath = path.join(targetPath, "package.json");
|
|
29
|
+
|
|
30
|
+
const spinner = ora("Configuring modules...").start();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const depsToRemove = [];
|
|
34
|
+
|
|
35
|
+
for (const [moduleName, moduleConfig] of Object.entries(MODULE_REGISTRY)) {
|
|
36
|
+
const modulePath = path.join(modulesPath, moduleConfig.folder);
|
|
37
|
+
const isSelected = selectedModules.includes(moduleName);
|
|
38
|
+
const exists = await fs.pathExists(modulePath);
|
|
39
|
+
|
|
40
|
+
if (!isSelected && exists) {
|
|
41
|
+
// Remove module folder
|
|
42
|
+
await fs.remove(modulePath);
|
|
43
|
+
logger.step(`removed module: ${moduleName}`);
|
|
44
|
+
|
|
45
|
+
// Queue dependencies for removal
|
|
46
|
+
depsToRemove.push(...moduleConfig.deps);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isSelected) {
|
|
50
|
+
logger.step(`included module: ${moduleName}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Remove unused dependencies from package.json
|
|
55
|
+
if (depsToRemove.length > 0) {
|
|
56
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
57
|
+
|
|
58
|
+
depsToRemove.forEach((dep) => {
|
|
59
|
+
if (packageJson.dependencies[dep]) {
|
|
60
|
+
delete packageJson.dependencies[dep];
|
|
61
|
+
logger.step(`removed dep: ${dep}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
spinner.succeed(chalk.green("Modules configured"));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
spinner.fail(chalk.red("Failed to configure modules"));
|
|
71
|
+
logger.error(error.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { configureModules, MODULE_REGISTRY };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
|
|
3
|
+
const logger = {
|
|
4
|
+
welcome() {
|
|
5
|
+
console.log(chalk.bold.blue("Welcome to create-rn-app 🚀"));
|
|
6
|
+
console.log(chalk.gray("Setting up your React Native project...\n"));
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
step(message) {
|
|
10
|
+
console.log(chalk.gray(` ${message}`));
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
success(answers) {
|
|
14
|
+
console.log("\n" + chalk.bold.green("✔ Project ready!"));
|
|
15
|
+
console.log(chalk.gray("─────────────────────────────────────"));
|
|
16
|
+
console.log(chalk.white(" Modules installed:"));
|
|
17
|
+
if (answers.selectedModules.length === 0) {
|
|
18
|
+
console.log(chalk.gray(" none — base template only"));
|
|
19
|
+
} else {
|
|
20
|
+
answers.selectedModules.forEach((m) =>
|
|
21
|
+
console.log(chalk.cyan(` ✔ ${m}`)),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
console.log(chalk.gray("─────────────────────────────────────"));
|
|
25
|
+
console.log(chalk.white(" Next steps:"));
|
|
26
|
+
console.log(chalk.cyan(` cd ${answers.projectName}`));
|
|
27
|
+
console.log(chalk.cyan(" npx expo start --ios"));
|
|
28
|
+
console.log(chalk.gray("─────────────────────────────────────\n"));
|
|
29
|
+
console.log(chalk.gray(" note: installed with --legacy-peer-deps"));
|
|
30
|
+
console.log(chalk.gray(" run: npx expo install --fix if needed\n"));
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
error(message) {
|
|
34
|
+
console.log(chalk.red(`✖ ${message}`));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = logger;
|