@highbeek/create-rnstarterkit 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/README.md ADDED
File without changes
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const appGenerator_1 = require("../src/generators/appGenerator");
9
+ async function main() {
10
+ console.log("🚀 Welcome to RNStarterKit!");
11
+ const answers = await inquirer_1.default.prompt([
12
+ {
13
+ type: "input",
14
+ name: "projectName",
15
+ message: "Project name:",
16
+ validate: (input) => /^[A-Za-z0-9]+$/.test(input)
17
+ ? true
18
+ : "Project name must be alphanumeric",
19
+ },
20
+ {
21
+ type: "list",
22
+ name: "platform",
23
+ message: "Choose platform",
24
+ choices: ["Expo", "React Native CLI"],
25
+ },
26
+ {
27
+ type: "confirm",
28
+ name: "typescript",
29
+ message: "Use TypeScript?",
30
+ default: true,
31
+ },
32
+ {
33
+ type: "confirm",
34
+ name: "absoluteImports",
35
+ message: "Use absolute import alias (@/*)?",
36
+ default: true,
37
+ },
38
+ {
39
+ type: "list",
40
+ name: "state",
41
+ message: "State management?",
42
+ choices: ["None", "Redux Toolkit", "Zustand"],
43
+ default: "None",
44
+ },
45
+ {
46
+ type: "list",
47
+ name: "dataFetching",
48
+ message: "Data fetching library?",
49
+ choices: ["None", "React Query", "SWR", "Axios"],
50
+ default: "None",
51
+ },
52
+ {
53
+ type: "checkbox",
54
+ name: "validation",
55
+ message: "Validation libraries?",
56
+ choices: ["Formik", "React Hook Form", "Yup"],
57
+ default: [],
58
+ },
59
+ {
60
+ type: "list",
61
+ name: "storage",
62
+ message: "Storage method?",
63
+ choices: ["AsyncStorage", "MMKV", "None"],
64
+ default: "AsyncStorage",
65
+ },
66
+ {
67
+ type: "confirm",
68
+ name: "auth",
69
+ message: "Include auth scaffold?",
70
+ default: false,
71
+ },
72
+ {
73
+ type: "confirm",
74
+ name: "apiClient",
75
+ message: "Include API client scaffold?",
76
+ default: true,
77
+ },
78
+ {
79
+ type: "confirm",
80
+ name: "ci",
81
+ message: "Include CI setup?",
82
+ default: false,
83
+ },
84
+ ]);
85
+ await (0, appGenerator_1.generateApp)(answers);
86
+ }
87
+ main().catch((error) => {
88
+ console.error("❌ Failed to create project.");
89
+ console.error(error);
90
+ process.exit(1);
91
+ });
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateApp = generateApp;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const execa_1 = require("execa");
10
+ async function generateApp(options) {
11
+ const { platform, projectName, auth, apiClient, absoluteImports, state, dataFetching, validation, storage, typescript, } = options;
12
+ const templateRoot = await resolveTemplateRoot();
13
+ const templateFolder = platform === "Expo" ? "expo-base" : "cli-base";
14
+ const templatePath = path_1.default.join(templateRoot, templateFolder);
15
+ const targetPath = path_1.default.join(process.cwd(), projectName);
16
+ console.log("📂 Creating project...");
17
+ await fs_extra_1.default.copy(templatePath, targetPath, {
18
+ filter: (src) => shouldCopyPath(src),
19
+ });
20
+ await replaceProjectName(targetPath, projectName);
21
+ await createStandardStructure(targetPath);
22
+ if (auth) {
23
+ const authFolder = state === "Redux Toolkit"
24
+ ? "auth-redux"
25
+ : state === "Zustand"
26
+ ? "auth-zustand"
27
+ : "auth-context";
28
+ await copyOptionalModule(authFolder, targetPath);
29
+ if (authFolder === "auth-context" ||
30
+ authFolder === "auth-zustand" ||
31
+ authFolder === "auth-redux") {
32
+ await setAuthTabsByPlatform(targetPath, platform);
33
+ }
34
+ }
35
+ if (apiClient) {
36
+ await copyOptionalModule("apiClient", targetPath);
37
+ await configureApiClientByPlatform(targetPath, platform);
38
+ }
39
+ if (state === "Redux Toolkit")
40
+ await copyOptionalModule("redux", targetPath);
41
+ if (state === "Zustand")
42
+ await copyOptionalModule("zustand", targetPath);
43
+ if (dataFetching === "React Query")
44
+ await copyOptionalModule("react-query", targetPath);
45
+ if (storage === "MMKV")
46
+ await copyOptionalModule("mmkv", targetPath);
47
+ await configureAbsoluteImports(targetPath, platform, absoluteImports);
48
+ await configureDataFetching(targetPath, dataFetching, apiClient);
49
+ await configureValidation(targetPath, validation);
50
+ if (typescript) {
51
+ const tsconfigTemplate = path_1.default.join(templateRoot, "optional/tsconfig.json");
52
+ if (await fs_extra_1.default.pathExists(tsconfigTemplate)) {
53
+ await fs_extra_1.default.copy(tsconfigTemplate, path_1.default.join(targetPath, "tsconfig.json"), {
54
+ overwrite: true,
55
+ });
56
+ }
57
+ const appJs = path_1.default.join(targetPath, "App.js");
58
+ const appTsx = path_1.default.join(targetPath, "App.tsx");
59
+ if (await fs_extra_1.default.pathExists(appJs)) {
60
+ await fs_extra_1.default.rename(appJs, appTsx);
61
+ }
62
+ }
63
+ await installDependencies(targetPath);
64
+ console.log("✅ Project created successfully!");
65
+ }
66
+ async function copyOptionalModule(moduleName, targetPath) {
67
+ const templateRoot = await resolveTemplateRoot();
68
+ const modulePath = path_1.default.join(templateRoot, "optional", moduleName);
69
+ if (await fs_extra_1.default.pathExists(modulePath)) {
70
+ await fs_extra_1.default.copy(modulePath, targetPath, { overwrite: true });
71
+ console.log(`➕ Added optional module: ${moduleName}`);
72
+ }
73
+ }
74
+ async function replaceProjectName(dir, projectName) {
75
+ const files = await fs_extra_1.default.readdir(dir);
76
+ for (const file of files) {
77
+ const filePath = path_1.default.join(dir, file);
78
+ const stat = await fs_extra_1.default.stat(filePath);
79
+ if (stat.isDirectory()) {
80
+ await replaceProjectName(filePath, projectName);
81
+ }
82
+ else {
83
+ if (!isTextFile(filePath))
84
+ continue;
85
+ const content = await fs_extra_1.default.readFile(filePath, "utf8");
86
+ const updated = content.replace(/{{projectName}}/g, projectName);
87
+ await fs_extra_1.default.writeFile(filePath, updated);
88
+ }
89
+ }
90
+ }
91
+ async function installDependencies(targetPath) {
92
+ console.log("📦 Installing dependencies...");
93
+ try {
94
+ await (0, execa_1.execa)("npm", ["install"], { cwd: targetPath, stdio: "inherit" });
95
+ console.log("✅ Dependencies installed!");
96
+ }
97
+ catch (err) {
98
+ console.log("⚠️ Failed to install dependencies. Run 'npm install' manually.");
99
+ }
100
+ }
101
+ async function setAuthTabsByPlatform(targetPath, platform) {
102
+ const navigationPath = path_1.default.join(targetPath, "navigation");
103
+ const targetTabs = path_1.default.join(navigationPath, "BottomTabs.tsx");
104
+ const tabSourceImport = platform === "Expo" ? "./expo/BottomTabs" : "./cli/BottomTabs";
105
+ if (!(await fs_extra_1.default.pathExists(navigationPath)))
106
+ return;
107
+ await fs_extra_1.default.writeFile(targetTabs, `export { default } from "${tabSourceImport}";\n`, "utf8");
108
+ }
109
+ async function configureApiClientByPlatform(targetPath, platform) {
110
+ const configPath = path_1.default.join(targetPath, "config");
111
+ const envSelectorPath = path_1.default.join(configPath, "env.ts");
112
+ const envImport = platform === "Expo" ? "./env.expo" : "./env.cli";
113
+ if (await fs_extra_1.default.pathExists(configPath)) {
114
+ await fs_extra_1.default.writeFile(envSelectorPath, `export { API_BASE_URL } from "${envImport}";\n`, "utf8");
115
+ }
116
+ await ensureApiEnvFiles(targetPath, platform);
117
+ if (platform === "React Native CLI") {
118
+ await ensureCliApiEnvSupport(targetPath);
119
+ }
120
+ }
121
+ async function ensureApiEnvFiles(targetPath, platform) {
122
+ const key = platform === "Expo" ? "EXPO_PUBLIC_API_URL" : "API_BASE_URL";
123
+ const envLine = `${key}=https://example.com/api\n`;
124
+ const envPath = path_1.default.join(targetPath, ".env");
125
+ const envExamplePath = path_1.default.join(targetPath, ".env.example");
126
+ if (!(await fs_extra_1.default.pathExists(envPath))) {
127
+ await fs_extra_1.default.writeFile(envPath, envLine, "utf8");
128
+ }
129
+ if (!(await fs_extra_1.default.pathExists(envExamplePath))) {
130
+ await fs_extra_1.default.writeFile(envExamplePath, envLine, "utf8");
131
+ }
132
+ }
133
+ async function ensureCliApiEnvSupport(targetPath) {
134
+ await ensureDependencies(targetPath, {
135
+ devDependencies: {
136
+ "react-native-dotenv": "^3.4.11",
137
+ },
138
+ });
139
+ }
140
+ async function createStandardStructure(targetPath) {
141
+ const directories = [
142
+ "assets",
143
+ "assets/icons",
144
+ "assets/images",
145
+ "assets/fonts",
146
+ "components",
147
+ "navigation",
148
+ "screens",
149
+ "hooks",
150
+ "utils",
151
+ "services",
152
+ ];
153
+ for (const directory of directories) {
154
+ const absoluteDir = path_1.default.join(targetPath, directory);
155
+ await fs_extra_1.default.ensureDir(absoluteDir);
156
+ const gitkeepPath = path_1.default.join(absoluteDir, ".gitkeep");
157
+ if (!(await fs_extra_1.default.pathExists(gitkeepPath))) {
158
+ await fs_extra_1.default.writeFile(gitkeepPath, "", "utf8");
159
+ }
160
+ }
161
+ }
162
+ async function configureAbsoluteImports(targetPath, platform, absoluteImports) {
163
+ await configureTsconfigAlias(targetPath, absoluteImports);
164
+ if (platform === "React Native CLI") {
165
+ if (absoluteImports) {
166
+ await ensureDependencies(targetPath, {
167
+ devDependencies: {
168
+ "babel-plugin-module-resolver": "^5.0.2",
169
+ },
170
+ });
171
+ }
172
+ await writeCliBabelConfig(targetPath, {
173
+ useDotenv: await usesDotenv(targetPath),
174
+ useAbsoluteImports: absoluteImports,
175
+ });
176
+ }
177
+ }
178
+ async function configureDataFetching(targetPath, dataFetching, apiClient) {
179
+ if (dataFetching === "React Query") {
180
+ await ensureDependencies(targetPath, {
181
+ dependencies: {
182
+ "@tanstack/react-query": "^5.90.5",
183
+ },
184
+ });
185
+ await writeIfMissing(path_1.default.join(targetPath, "services/queryClient.ts"), `import { QueryClient } from "@tanstack/react-query";
186
+
187
+ export const queryClient = new QueryClient();
188
+ `);
189
+ }
190
+ if (dataFetching === "SWR") {
191
+ await ensureDependencies(targetPath, {
192
+ dependencies: {
193
+ swr: "^2.3.6",
194
+ },
195
+ });
196
+ }
197
+ if (dataFetching === "Axios") {
198
+ await ensureDependencies(targetPath, {
199
+ dependencies: {
200
+ axios: "^1.12.2",
201
+ },
202
+ });
203
+ await writeIfMissing(path_1.default.join(targetPath, "api/axiosClient.ts"), `import axios from "axios";
204
+ import { API_BASE_URL } from "../config/env";
205
+
206
+ export const axiosClient = axios.create({
207
+ baseURL: API_BASE_URL,
208
+ timeout: 15000,
209
+ });
210
+ `);
211
+ if (apiClient) {
212
+ await fs_extra_1.default.writeFile(path_1.default.join(targetPath, "api/client.ts"), `import axios from "axios";
213
+ import { API_BASE_URL } from "../config/env";
214
+
215
+ export class ApiError extends Error {
216
+ status: number;
217
+ data: unknown;
218
+
219
+ constructor(status: number, message: string, data: unknown) {
220
+ super(message);
221
+ this.name = "ApiError";
222
+ this.status = status;
223
+ this.data = data;
224
+ }
225
+ }
226
+
227
+ type RequestOptions = {
228
+ headers?: Record<string, string>;
229
+ query?: Record<string, string | number | boolean | undefined>;
230
+ token?: string | null;
231
+ signal?: AbortSignal;
232
+ };
233
+
234
+ const client = axios.create({
235
+ baseURL: API_BASE_URL,
236
+ timeout: 15000,
237
+ });
238
+
239
+ function resolveErrorMessage(data: unknown, status: number) {
240
+ if (
241
+ typeof data === "object" &&
242
+ data !== null &&
243
+ "message" in data &&
244
+ typeof (data as { message: unknown }).message === "string"
245
+ ) {
246
+ return (data as { message: string }).message;
247
+ }
248
+ return \`Request failed with status \${status}\`;
249
+ }
250
+
251
+ async function request<T>(
252
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
253
+ path: string,
254
+ body?: unknown,
255
+ options: RequestOptions = {},
256
+ ): Promise<T> {
257
+ const { headers = {}, query, token, signal } = options;
258
+ try {
259
+ const response = await client.request<T>({
260
+ method,
261
+ url: path,
262
+ data: body,
263
+ params: query,
264
+ signal,
265
+ headers: {
266
+ ...(token ? { Authorization: \`Bearer \${token}\` } : {}),
267
+ ...headers,
268
+ },
269
+ });
270
+ return response.data;
271
+ } catch (error) {
272
+ if (axios.isAxiosError(error)) {
273
+ const status = error.response?.status ?? 500;
274
+ const data = error.response?.data ?? null;
275
+ throw new ApiError(status, resolveErrorMessage(data, status), data);
276
+ }
277
+ throw error;
278
+ }
279
+ }
280
+
281
+ export const apiClient = {
282
+ get: <T>(path: string, options?: RequestOptions) =>
283
+ request<T>("GET", path, undefined, options),
284
+ post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
285
+ request<T>("POST", path, body, options),
286
+ put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
287
+ request<T>("PUT", path, body, options),
288
+ patch: <T>(path: string, body?: unknown, options?: RequestOptions) =>
289
+ request<T>("PATCH", path, body, options),
290
+ delete: <T>(path: string, options?: RequestOptions) =>
291
+ request<T>("DELETE", path, undefined, options),
292
+ };
293
+ `, "utf8");
294
+ }
295
+ }
296
+ }
297
+ async function configureValidation(targetPath, validation) {
298
+ const dependencies = {};
299
+ if (validation.includes("Formik"))
300
+ dependencies.formik = "^2.4.6";
301
+ if (validation.includes("React Hook Form"))
302
+ dependencies["react-hook-form"] = "^7.62.0";
303
+ if (validation.includes("Yup"))
304
+ dependencies.yup = "^1.7.0";
305
+ if (Object.keys(dependencies).length > 0) {
306
+ await ensureDependencies(targetPath, { dependencies });
307
+ }
308
+ }
309
+ async function configureTsconfigAlias(targetPath, absoluteImports) {
310
+ const tsconfigPath = path_1.default.join(targetPath, "tsconfig.json");
311
+ if (!(await fs_extra_1.default.pathExists(tsconfigPath)))
312
+ return;
313
+ const tsconfig = await fs_extra_1.default.readJson(tsconfigPath);
314
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
315
+ if (absoluteImports) {
316
+ tsconfig.compilerOptions.baseUrl = ".";
317
+ tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
318
+ tsconfig.compilerOptions.paths["@/*"] = ["./*"];
319
+ }
320
+ else if (tsconfig.compilerOptions.paths) {
321
+ delete tsconfig.compilerOptions.paths["@/*"];
322
+ if (Object.keys(tsconfig.compilerOptions.paths).length === 0) {
323
+ delete tsconfig.compilerOptions.paths;
324
+ }
325
+ }
326
+ await fs_extra_1.default.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
327
+ }
328
+ async function writeCliBabelConfig(targetPath, options) {
329
+ const babelConfigPath = path_1.default.join(targetPath, "babel.config.js");
330
+ if (!(await fs_extra_1.default.pathExists(babelConfigPath)))
331
+ return;
332
+ const plugins = [];
333
+ if (options.useDotenv)
334
+ plugins.push("'module:react-native-dotenv'");
335
+ if (options.useAbsoluteImports) {
336
+ plugins.push("['module-resolver', { root: ['./'], alias: { '@': './' } }]");
337
+ }
338
+ const pluginsLine = plugins.length > 0 ? `,\n plugins: [${plugins.join(", ")}]` : "";
339
+ const content = `module.exports = {\n presets: ['module:@react-native/babel-preset']${pluginsLine},\n};\n`;
340
+ await fs_extra_1.default.writeFile(babelConfigPath, content, "utf8");
341
+ }
342
+ async function usesDotenv(targetPath) {
343
+ const envCliPath = path_1.default.join(targetPath, "config/env.cli.ts");
344
+ return fs_extra_1.default.pathExists(envCliPath);
345
+ }
346
+ async function ensureDependencies(targetPath, patch) {
347
+ const packageJsonPath = path_1.default.join(targetPath, "package.json");
348
+ if (await fs_extra_1.default.pathExists(packageJsonPath)) {
349
+ const packageJson = await fs_extra_1.default.readJson(packageJsonPath);
350
+ if (patch.dependencies) {
351
+ packageJson.dependencies = packageJson.dependencies || {};
352
+ for (const [pkg, version] of Object.entries(patch.dependencies)) {
353
+ if (!packageJson.dependencies[pkg]) {
354
+ packageJson.dependencies[pkg] = version;
355
+ }
356
+ }
357
+ }
358
+ if (patch.devDependencies) {
359
+ packageJson.devDependencies = packageJson.devDependencies || {};
360
+ for (const [pkg, version] of Object.entries(patch.devDependencies)) {
361
+ if (!packageJson.devDependencies[pkg]) {
362
+ packageJson.devDependencies[pkg] = version;
363
+ }
364
+ }
365
+ }
366
+ await fs_extra_1.default.writeJson(packageJsonPath, packageJson, { spaces: 2 });
367
+ }
368
+ }
369
+ async function writeIfMissing(filePath, content) {
370
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
371
+ if (!(await fs_extra_1.default.pathExists(filePath))) {
372
+ await fs_extra_1.default.writeFile(filePath, content, "utf8");
373
+ }
374
+ }
375
+ async function resolveTemplateRoot() {
376
+ const candidates = [
377
+ path_1.default.join(__dirname, "../templates"), // ts-node: src/generators -> src/templates
378
+ path_1.default.join(__dirname, "../../templates"), // built output layouts
379
+ ];
380
+ for (const candidate of candidates) {
381
+ if (await fs_extra_1.default.pathExists(candidate))
382
+ return candidate;
383
+ }
384
+ throw new Error("Template root not found.");
385
+ }
386
+ function shouldCopyPath(src) {
387
+ const normalized = src.replace(/\\/g, "/");
388
+ const blockedSegments = [
389
+ "/.git/",
390
+ "/node_modules/",
391
+ "/ios/Pods/",
392
+ "/android/.gradle/",
393
+ "/vendor/bundle/",
394
+ ];
395
+ if (normalized.endsWith("/.git"))
396
+ return false;
397
+ return !blockedSegments.some((segment) => normalized.includes(segment));
398
+ }
399
+ function isTextFile(filePath) {
400
+ const extension = path_1.default.extname(filePath).toLowerCase();
401
+ const textExtensions = new Set([
402
+ ".js",
403
+ ".jsx",
404
+ ".ts",
405
+ ".tsx",
406
+ ".json",
407
+ ".md",
408
+ ".xml",
409
+ ".gradle",
410
+ ".properties",
411
+ ".kt",
412
+ ".kts",
413
+ ".swift",
414
+ ".m",
415
+ ".mm",
416
+ ".h",
417
+ ".java",
418
+ ".rb",
419
+ ".plist",
420
+ ".pbxproj",
421
+ ".yml",
422
+ ".yaml",
423
+ ".txt",
424
+ ".config",
425
+ ".lock",
426
+ ".xcprivacy",
427
+ ".storyboard",
428
+ ]);
429
+ const base = path_1.default.basename(filePath).toLowerCase();
430
+ return textExtensions.has(extension) || base === "podfile" || base === "gemfile";
431
+ }
@@ -0,0 +1 @@
1
+ "use strict";
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@highbeek/create-rnstarterkit",
3
+ "version": "1.0.0",
4
+ "description": "CLI to scaffold production-ready React Native app structures.",
5
+ "main": "dist/src/generators/appGenerator.js",
6
+ "bin": {
7
+ "create-rnstarterkit": "dist/bin/create-rnstarterkit.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.build.json",
15
+ "prepublishOnly": "npm run build",
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "keywords": [],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "type": "commonjs",
22
+ "dependencies": {
23
+ "chalk": "^5.6.2",
24
+ "commander": "^14.0.3",
25
+ "execa": "^9.6.1",
26
+ "fs-extra": "^11.3.3",
27
+ "inquirer": "^13.2.5"
28
+ },
29
+ "devDependencies": {
30
+ "@types/inquirer": "^9.0.9",
31
+ "@types/node": "^25.2.3",
32
+ "ts-node": "^10.9.2",
33
+ "typescript": "^5.9.3"
34
+ }
35
+ }