@highbeek/create-rnstarterkit 1.0.0 → 1.0.1-beta.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.
Files changed (121) hide show
  1. package/dist/bin/create-rnstarterkit.js +21 -7
  2. package/dist/src/generators/appGenerator.js +976 -60
  3. package/dist/templates/cli-base/.bundle/config +2 -0
  4. package/dist/templates/cli-base/.eslintrc.js +4 -0
  5. package/dist/templates/cli-base/.prettierrc.js +5 -0
  6. package/dist/templates/cli-base/.watchmanconfig +1 -0
  7. package/dist/templates/cli-base/App.tsx +45 -0
  8. package/dist/templates/cli-base/Gemfile +16 -0
  9. package/dist/templates/cli-base/Gemfile.lock +169 -0
  10. package/dist/templates/cli-base/README.md +97 -0
  11. package/dist/templates/cli-base/__tests__/App.test.tsx +13 -0
  12. package/dist/templates/cli-base/android/app/build.gradle +119 -0
  13. package/dist/templates/cli-base/android/app/debug.keystore +0 -0
  14. package/dist/templates/cli-base/android/app/proguard-rules.pro +10 -0
  15. package/dist/templates/cli-base/android/app/src/main/AndroidManifest.xml +27 -0
  16. package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainActivity.kt +22 -0
  17. package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainApplication.kt +27 -0
  18. package/dist/templates/cli-base/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  19. package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  20. package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  21. package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  22. package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  23. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  24. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  25. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  26. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  27. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  28. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  29. package/dist/templates/cli-base/android/app/src/main/res/values/strings.xml +3 -0
  30. package/dist/templates/cli-base/android/app/src/main/res/values/styles.xml +9 -0
  31. package/dist/templates/cli-base/android/build.gradle +21 -0
  32. package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  33. package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  34. package/dist/templates/cli-base/android/gradle.properties +44 -0
  35. package/dist/templates/cli-base/android/gradlew +251 -0
  36. package/dist/templates/cli-base/android/gradlew.bat +99 -0
  37. package/dist/templates/cli-base/android/settings.gradle +6 -0
  38. package/dist/templates/cli-base/app.json +4 -0
  39. package/dist/templates/cli-base/babel.config.js +3 -0
  40. package/dist/templates/cli-base/index.js +9 -0
  41. package/dist/templates/cli-base/ios/.xcode.env +11 -0
  42. package/dist/templates/cli-base/ios/BaseApp/AppDelegate.swift +48 -0
  43. package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  44. package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/Contents.json +6 -0
  45. package/dist/templates/cli-base/ios/BaseApp/Info.plist +60 -0
  46. package/dist/templates/cli-base/ios/BaseApp/LaunchScreen.storyboard +47 -0
  47. package/dist/templates/cli-base/ios/BaseApp/PrivacyInfo.xcprivacy +37 -0
  48. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +494 -0
  49. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/xcshareddata/xcschemes/BaseApp.xcscheme +88 -0
  50. package/dist/templates/cli-base/ios/BaseApp.xcworkspace/contents.xcworkspacedata +10 -0
  51. package/dist/templates/cli-base/ios/Podfile +34 -0
  52. package/dist/templates/cli-base/ios/Podfile.lock +2165 -0
  53. package/dist/templates/cli-base/jest.config.js +3 -0
  54. package/dist/templates/cli-base/metro.config.js +11 -0
  55. package/dist/templates/cli-base/package-lock.json +11859 -0
  56. package/dist/templates/cli-base/package.json +41 -0
  57. package/dist/templates/cli-base/tsconfig.json +8 -0
  58. package/dist/templates/expo-base/.vscode/extensions.json +1 -0
  59. package/dist/templates/expo-base/.vscode/settings.json +7 -0
  60. package/dist/templates/expo-base/README.md +50 -0
  61. package/dist/templates/expo-base/app/_layout.tsx +12 -0
  62. package/dist/templates/expo-base/app/index.tsx +9 -0
  63. package/dist/templates/expo-base/app.json +48 -0
  64. package/dist/templates/expo-base/assets/images/android-icon-background.png +0 -0
  65. package/dist/templates/expo-base/assets/images/android-icon-foreground.png +0 -0
  66. package/dist/templates/expo-base/assets/images/android-icon-monochrome.png +0 -0
  67. package/dist/templates/expo-base/assets/images/favicon.png +0 -0
  68. package/dist/templates/expo-base/assets/images/icon.png +0 -0
  69. package/dist/templates/expo-base/assets/images/partial-react-logo.png +0 -0
  70. package/dist/templates/expo-base/assets/images/react-logo.png +0 -0
  71. package/dist/templates/expo-base/assets/images/react-logo@2x.png +0 -0
  72. package/dist/templates/expo-base/assets/images/react-logo@3x.png +0 -0
  73. package/dist/templates/expo-base/assets/images/splash-icon.png +0 -0
  74. package/dist/templates/expo-base/components/ui/collapsible.tsx +45 -0
  75. package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +32 -0
  76. package/dist/templates/expo-base/components/ui/icon-symbol.tsx +41 -0
  77. package/dist/templates/expo-base/eslint.config.js +10 -0
  78. package/dist/templates/expo-base/package-lock.json +12916 -0
  79. package/dist/templates/expo-base/package.json +46 -0
  80. package/dist/templates/expo-base/tsconfig.json +17 -0
  81. package/dist/templates/optional/apiClient/api/client.ts +142 -0
  82. package/dist/templates/optional/apiClient/api/index.ts +1 -0
  83. package/dist/templates/optional/apiClient/config/env.cli.ts +5 -0
  84. package/dist/templates/optional/apiClient/config/env.expo.ts +4 -0
  85. package/dist/templates/optional/apiClient/config/env.ts +1 -0
  86. package/dist/templates/optional/apiClient/env.d.ts +3 -0
  87. package/dist/templates/optional/auth-context/api/endpoints/auth.ts +14 -0
  88. package/dist/templates/optional/auth-context/context/AuthContext.tsx +47 -0
  89. package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +38 -0
  90. package/dist/templates/optional/auth-context/navigation/cli/BottomTabs.tsx +17 -0
  91. package/dist/templates/optional/auth-context/navigation/expo/BottomTabs.tsx +29 -0
  92. package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +15 -0
  93. package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +63 -0
  94. package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +11 -0
  95. package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +63 -0
  96. package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +11 -0
  97. package/dist/templates/optional/auth-context/utils/storage.ts +13 -0
  98. package/dist/templates/optional/auth-redux/api/endpoints/auth.ts +14 -0
  99. package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +30 -0
  100. package/dist/templates/optional/auth-redux/navigation/cli/BottomTabs.tsx +17 -0
  101. package/dist/templates/optional/auth-redux/navigation/expo/BottomTabs.tsx +31 -0
  102. package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +16 -0
  103. package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +64 -0
  104. package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +15 -0
  105. package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +64 -0
  106. package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +15 -0
  107. package/dist/templates/optional/auth-redux/store/authSlice.ts +25 -0
  108. package/dist/templates/optional/auth-redux/store/store.ts +11 -0
  109. package/dist/templates/optional/auth-zustand/api/endpoints/auth.ts +14 -0
  110. package/dist/templates/optional/auth-zustand/navigation/BottomTabs.tsx +1 -0
  111. package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +44 -0
  112. package/dist/templates/optional/auth-zustand/navigation/cli/BottomTabs.tsx +17 -0
  113. package/dist/templates/optional/auth-zustand/navigation/expo/BottomTabs.tsx +31 -0
  114. package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +15 -0
  115. package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +63 -0
  116. package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +11 -0
  117. package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +63 -0
  118. package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +11 -0
  119. package/dist/templates/optional/auth-zustand/store/authStore.ts +30 -0
  120. package/dist/templates/optional/auth-zustand/utils/storage.ts +13 -0
  121. package/package.json +2 -2
@@ -8,17 +8,17 @@ const path_1 = __importDefault(require("path"));
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const execa_1 = require("execa");
10
10
  async function generateApp(options) {
11
- const { platform, projectName, auth, apiClient, absoluteImports, state, dataFetching, validation, storage, typescript, } = options;
11
+ const { platform, projectName, auth, apiClient, absoluteImports, state, dataFetching, validation, storage, typescript, apiClientType, } = options;
12
12
  const templateRoot = await resolveTemplateRoot();
13
13
  const templateFolder = platform === "Expo" ? "expo-base" : "cli-base";
14
14
  const templatePath = path_1.default.join(templateRoot, templateFolder);
15
15
  const targetPath = path_1.default.join(process.cwd(), projectName);
16
16
  console.log("📂 Creating project...");
17
17
  await fs_extra_1.default.copy(templatePath, targetPath, {
18
- filter: (src) => shouldCopyPath(src),
18
+ filter: (src) => shouldCopyPath(src, templatePath),
19
19
  });
20
20
  await replaceProjectName(targetPath, projectName);
21
- await createStandardStructure(targetPath);
21
+ await createStandardStructure(targetPath, platform);
22
22
  if (auth) {
23
23
  const authFolder = state === "Redux Toolkit"
24
24
  ? "auth-redux"
@@ -29,7 +29,13 @@ async function generateApp(options) {
29
29
  if (authFolder === "auth-context" ||
30
30
  authFolder === "auth-zustand" ||
31
31
  authFolder === "auth-redux") {
32
- await setAuthTabsByPlatform(targetPath, platform);
32
+ if (platform === "Expo") {
33
+ await writeExpoRouterAuthRoutes(targetPath, state);
34
+ }
35
+ else {
36
+ await setAuthTabsByPlatform(targetPath, platform);
37
+ await writeAuthAppShell(targetPath, state);
38
+ }
33
39
  }
34
40
  }
35
41
  if (apiClient) {
@@ -44,8 +50,16 @@ async function generateApp(options) {
44
50
  await copyOptionalModule("react-query", targetPath);
45
51
  if (storage === "MMKV")
46
52
  await copyOptionalModule("mmkv", targetPath);
53
+ await configureStateAndAuthDependencies(targetPath, {
54
+ platform,
55
+ state,
56
+ auth,
57
+ storage,
58
+ });
47
59
  await configureAbsoluteImports(targetPath, platform, absoluteImports);
48
- await configureDataFetching(targetPath, dataFetching, apiClient);
60
+ await configureDataFetching(targetPath, dataFetching);
61
+ await configureApiClientTransport(targetPath, apiClient, apiClientType);
62
+ await syncApiIndex(targetPath);
49
63
  await configureValidation(targetPath, validation);
50
64
  if (typescript) {
51
65
  const tsconfigTemplate = path_1.default.join(templateRoot, "optional/tsconfig.json");
@@ -137,26 +151,26 @@ async function ensureCliApiEnvSupport(targetPath) {
137
151
  },
138
152
  });
139
153
  }
140
- async function createStandardStructure(targetPath) {
141
- const directories = [
154
+ async function createStandardStructure(targetPath, platform) {
155
+ const commonDirectories = [
142
156
  "assets",
143
157
  "assets/icons",
144
158
  "assets/images",
145
159
  "assets/fonts",
146
160
  "components",
147
- "navigation",
148
- "screens",
149
161
  "hooks",
150
162
  "utils",
151
163
  "services",
164
+ "api",
165
+ "config",
166
+ "context",
152
167
  ];
168
+ const cliOnlyDirectories = ["navigation", "screens"];
169
+ const directories = platform === "Expo"
170
+ ? commonDirectories
171
+ : [...commonDirectories, ...cliOnlyDirectories];
153
172
  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
- }
173
+ await fs_extra_1.default.ensureDir(path_1.default.join(targetPath, directory));
160
174
  }
161
175
  }
162
176
  async function configureAbsoluteImports(targetPath, platform, absoluteImports) {
@@ -175,7 +189,7 @@ async function configureAbsoluteImports(targetPath, platform, absoluteImports) {
175
189
  });
176
190
  }
177
191
  }
178
- async function configureDataFetching(targetPath, dataFetching, apiClient) {
192
+ async function configureDataFetching(targetPath, dataFetching) {
179
193
  if (dataFetching === "React Query") {
180
194
  await ensureDependencies(targetPath, {
181
195
  dependencies: {
@@ -194,22 +208,20 @@ export const queryClient = new QueryClient();
194
208
  },
195
209
  });
196
210
  }
197
- if (dataFetching === "Axios") {
211
+ }
212
+ async function configureApiClientTransport(targetPath, apiClient, apiClientType) {
213
+ if (apiClient && apiClientType === "Axios") {
198
214
  await ensureDependencies(targetPath, {
199
215
  dependencies: {
200
216
  axios: "^1.12.2",
201
217
  },
202
218
  });
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";
219
+ await fs_extra_1.default.writeFile(path_1.default.join(targetPath, "api/client.ts"), `import axios, {
220
+ AxiosError,
221
+ AxiosRequestConfig,
222
+ AxiosResponse,
223
+ InternalAxiosRequestConfig,
224
+ } from "axios";
213
225
  import { API_BASE_URL } from "../config/env";
214
226
 
215
227
  export class ApiError extends Error {
@@ -236,6 +248,12 @@ const client = axios.create({
236
248
  timeout: 15000,
237
249
  });
238
250
 
251
+ let authTokenGetter: (() => string | null | undefined) | null = null;
252
+
253
+ export function setAuthTokenGetter(getter: (() => string | null | undefined) | null) {
254
+ authTokenGetter = getter;
255
+ }
256
+
239
257
  function resolveErrorMessage(data: unknown, status: number) {
240
258
  if (
241
259
  typeof data === "object" &&
@@ -248,6 +266,45 @@ function resolveErrorMessage(data: unknown, status: number) {
248
266
  return \`Request failed with status \${status}\`;
249
267
  }
250
268
 
269
+ export function addRequestInterceptor(
270
+ interceptor: (
271
+ config: InternalAxiosRequestConfig,
272
+ ) =>
273
+ | InternalAxiosRequestConfig
274
+ | Promise<InternalAxiosRequestConfig>,
275
+ ) {
276
+ return client.interceptors.request.use(interceptor);
277
+ }
278
+
279
+ export function addResponseInterceptor(
280
+ onFulfilled?: (
281
+ response: AxiosResponse,
282
+ ) => AxiosResponse | Promise<AxiosResponse>,
283
+ onRejected?: (error: AxiosError) => unknown,
284
+ ) {
285
+ return client.interceptors.response.use(onFulfilled, onRejected);
286
+ }
287
+
288
+ addRequestInterceptor((config) => {
289
+ const token = authTokenGetter?.();
290
+ if (token) {
291
+ config.headers.Authorization = \`Bearer \${token}\`;
292
+ }
293
+ return config;
294
+ });
295
+
296
+ addResponseInterceptor(
297
+ (response) => response,
298
+ (error) => {
299
+ if (axios.isAxiosError(error)) {
300
+ const status = error.response?.status ?? 500;
301
+ const data = error.response?.data ?? null;
302
+ return Promise.reject(new ApiError(status, resolveErrorMessage(data, status), data));
303
+ }
304
+ return Promise.reject(error);
305
+ },
306
+ );
307
+
251
308
  async function request<T>(
252
309
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
253
310
  path: string,
@@ -255,27 +312,19 @@ async function request<T>(
255
312
  options: RequestOptions = {},
256
313
  ): Promise<T> {
257
314
  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
- }
315
+ const requestConfig: AxiosRequestConfig = {
316
+ method,
317
+ url: path,
318
+ data: body,
319
+ params: query,
320
+ signal,
321
+ headers: {
322
+ ...(token ? { Authorization: \`Bearer \${token}\` } : {}),
323
+ ...headers,
324
+ },
325
+ };
326
+ const response = await client.request<T>(requestConfig);
327
+ return response.data;
279
328
  }
280
329
 
281
330
  export const apiClient = {
@@ -291,8 +340,27 @@ export const apiClient = {
291
340
  request<T>("DELETE", path, undefined, options),
292
341
  };
293
342
  `, "utf8");
294
- }
343
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "api/client.axios.ts"));
344
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "api/axiosClient.ts"));
345
+ }
346
+ }
347
+ async function syncApiIndex(targetPath) {
348
+ const apiDir = path_1.default.join(targetPath, "api");
349
+ if (!(await fs_extra_1.default.pathExists(apiDir)))
350
+ return;
351
+ const exports = [];
352
+ if (await fs_extra_1.default.pathExists(path_1.default.join(apiDir, "client.ts"))) {
353
+ exports.push('export * from "./client";');
295
354
  }
355
+ if (await fs_extra_1.default.pathExists(path_1.default.join(apiDir, "endpoints/auth.ts"))) {
356
+ exports.push('export * from "./endpoints/auth";');
357
+ }
358
+ else if (await fs_extra_1.default.pathExists(path_1.default.join(apiDir, "authApi.ts"))) {
359
+ exports.push('export * from "./authApi";');
360
+ }
361
+ if (exports.length === 0)
362
+ return;
363
+ await fs_extra_1.default.writeFile(path_1.default.join(apiDir, "index.ts"), `${exports.join("\n")}\n`, "utf8");
296
364
  }
297
365
  async function configureValidation(targetPath, validation) {
298
366
  const dependencies = {};
@@ -306,6 +374,850 @@ async function configureValidation(targetPath, validation) {
306
374
  await ensureDependencies(targetPath, { dependencies });
307
375
  }
308
376
  }
377
+ async function configureStateAndAuthDependencies(targetPath, options) {
378
+ const dependencies = {};
379
+ if (options.auth && options.platform === "React Native CLI") {
380
+ dependencies["@react-navigation/native-stack"] = "^7.3.29";
381
+ }
382
+ if (options.state === "Redux Toolkit") {
383
+ dependencies["@reduxjs/toolkit"] = "^2.9.0";
384
+ dependencies["react-redux"] = "^9.2.0";
385
+ }
386
+ if (options.state === "Zustand") {
387
+ dependencies.zustand = "^5.0.8";
388
+ }
389
+ if (options.storage === "AsyncStorage" &&
390
+ (options.state === "Context API" || options.state === "Zustand")) {
391
+ dependencies["@react-native-async-storage/async-storage"] = "^2.2.0";
392
+ }
393
+ if (Object.keys(dependencies).length > 0) {
394
+ await ensureDependencies(targetPath, { dependencies });
395
+ }
396
+ }
397
+ async function writeExpoRouterAuthRoutes(targetPath, state) {
398
+ const appDir = path_1.default.join(targetPath, "app");
399
+ const authDir = path_1.default.join(appDir, "(auth)");
400
+ const tabsDir = path_1.default.join(appDir, "(tabs)");
401
+ await fs_extra_1.default.ensureDir(authDir);
402
+ await fs_extra_1.default.ensureDir(tabsDir);
403
+ const stateBindings = getExpoAuthStateBindings(state);
404
+ const routeFiles = getExpoAuthRouteFiles(state);
405
+ await fs_extra_1.default.writeFile(path_1.default.join(appDir, "_layout.tsx"), stateBindings.rootLayout, "utf8");
406
+ await fs_extra_1.default.writeFile(path_1.default.join(appDir, "index.tsx"), stateBindings.indexRoute, "utf8");
407
+ await fs_extra_1.default.writeFile(path_1.default.join(authDir, "_layout.tsx"), stateBindings.authLayout, "utf8");
408
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "_layout.tsx"), stateBindings.tabsLayout, "utf8");
409
+ await fs_extra_1.default.writeFile(path_1.default.join(authDir, "login.tsx"), routeFiles.login, "utf8");
410
+ await fs_extra_1.default.writeFile(path_1.default.join(authDir, "register.tsx"), routeFiles.register, "utf8");
411
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "index.tsx"), routeFiles.home, "utf8");
412
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "profile.tsx"), routeFiles.profile, "utf8");
413
+ await fs_extra_1.default.writeFile(path_1.default.join(tabsDir, "settings.tsx"), routeFiles.settings, "utf8");
414
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "navigation"));
415
+ await fs_extra_1.default.remove(path_1.default.join(targetPath, "screens"));
416
+ }
417
+ function getExpoAuthStateBindings(state) {
418
+ if (state === "Redux Toolkit") {
419
+ return {
420
+ rootLayout: `import React from "react";
421
+ import { Slot } from "expo-router";
422
+ import { Provider } from "react-redux";
423
+ import { SafeAreaProvider } from "react-native-safe-area-context";
424
+ import { store } from "../store/store";
425
+
426
+ export default function RootLayout() {
427
+ return (
428
+ <SafeAreaProvider>
429
+ <Provider store={store}>
430
+ <Slot />
431
+ </Provider>
432
+ </SafeAreaProvider>
433
+ );
434
+ }
435
+ `,
436
+ indexRoute: `import React from "react";
437
+ import { Redirect } from "expo-router";
438
+ import { useSelector } from "react-redux";
439
+ import type { RootState } from "../store/store";
440
+
441
+ export default function Index() {
442
+ const token = useSelector((state: RootState) => state.auth.token);
443
+ return <Redirect href={token ? "/(tabs)" : "/(auth)/login"} />;
444
+ }
445
+ `,
446
+ authLayout: `import React from "react";
447
+ import { Redirect, Stack } from "expo-router";
448
+ import { useSelector } from "react-redux";
449
+ import type { RootState } from "../../store/store";
450
+
451
+ export default function AuthLayout() {
452
+ const token = useSelector((state: RootState) => state.auth.token);
453
+ if (token) return <Redirect href="/(tabs)" />;
454
+
455
+ return (
456
+ <Stack screenOptions={{ headerShown: false }}>
457
+ <Stack.Screen name="login" />
458
+ <Stack.Screen name="register" />
459
+ </Stack>
460
+ );
461
+ }
462
+ `,
463
+ tabsLayout: `import React from "react";
464
+ import { Redirect, Tabs } from "expo-router";
465
+ import { useSelector } from "react-redux";
466
+ import type { RootState } from "../../store/store";
467
+
468
+ export default function TabsLayout() {
469
+ const token = useSelector((state: RootState) => state.auth.token);
470
+ if (!token) return <Redirect href="/(auth)/login" />;
471
+
472
+ return (
473
+ <Tabs screenOptions={{ headerShown: false }}>
474
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
475
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
476
+ <Tabs.Screen name="settings" options={{ title: "Settings" }} />
477
+ </Tabs>
478
+ );
479
+ }
480
+ `,
481
+ };
482
+ }
483
+ if (state === "Zustand") {
484
+ return {
485
+ rootLayout: `import React from "react";
486
+ import { Slot } from "expo-router";
487
+ import { SafeAreaProvider } from "react-native-safe-area-context";
488
+
489
+ export default function RootLayout() {
490
+ return (
491
+ <SafeAreaProvider>
492
+ <Slot />
493
+ </SafeAreaProvider>
494
+ );
495
+ }
496
+ `,
497
+ indexRoute: `import React from "react";
498
+ import { useEffect } from "react";
499
+ import { ActivityIndicator, View } from "react-native";
500
+ import { Redirect } from "expo-router";
501
+ import { useAuthStore } from "../store/authStore";
502
+
503
+ export default function Index() {
504
+ const token = useAuthStore((state) => state.token);
505
+ const isHydrated = useAuthStore((state) => state.isHydrated);
506
+ const hydrate = useAuthStore((state) => state.hydrate);
507
+
508
+ useEffect(() => {
509
+ void hydrate();
510
+ }, [hydrate]);
511
+
512
+ if (!isHydrated) {
513
+ return (
514
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
515
+ <ActivityIndicator />
516
+ </View>
517
+ );
518
+ }
519
+
520
+ return <Redirect href={token ? "/(tabs)" : "/(auth)/login"} />;
521
+ }
522
+ `,
523
+ authLayout: `import React, { useEffect } from "react";
524
+ import { ActivityIndicator, View } from "react-native";
525
+ import { Redirect, Stack } from "expo-router";
526
+ import { useAuthStore } from "../../store/authStore";
527
+
528
+ export default function AuthLayout() {
529
+ const token = useAuthStore((state) => state.token);
530
+ const isHydrated = useAuthStore((state) => state.isHydrated);
531
+ const hydrate = useAuthStore((state) => state.hydrate);
532
+
533
+ useEffect(() => {
534
+ void hydrate();
535
+ }, [hydrate]);
536
+
537
+ if (!isHydrated) {
538
+ return (
539
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
540
+ <ActivityIndicator />
541
+ </View>
542
+ );
543
+ }
544
+
545
+ if (token) return <Redirect href="/(tabs)" />;
546
+
547
+ return (
548
+ <Stack screenOptions={{ headerShown: false }}>
549
+ <Stack.Screen name="login" />
550
+ <Stack.Screen name="register" />
551
+ </Stack>
552
+ );
553
+ }
554
+ `,
555
+ tabsLayout: `import React, { useEffect } from "react";
556
+ import { ActivityIndicator, View } from "react-native";
557
+ import { Redirect, Tabs } from "expo-router";
558
+ import { useAuthStore } from "../../store/authStore";
559
+
560
+ export default function TabsLayout() {
561
+ const token = useAuthStore((state) => state.token);
562
+ const isHydrated = useAuthStore((state) => state.isHydrated);
563
+ const hydrate = useAuthStore((state) => state.hydrate);
564
+
565
+ useEffect(() => {
566
+ void hydrate();
567
+ }, [hydrate]);
568
+
569
+ if (!isHydrated) {
570
+ return (
571
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
572
+ <ActivityIndicator />
573
+ </View>
574
+ );
575
+ }
576
+
577
+ if (!token) return <Redirect href="/(auth)/login" />;
578
+
579
+ return (
580
+ <Tabs screenOptions={{ headerShown: false }}>
581
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
582
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
583
+ <Tabs.Screen name="settings" options={{ title: "Settings" }} />
584
+ </Tabs>
585
+ );
586
+ }
587
+ `,
588
+ };
589
+ }
590
+ return {
591
+ rootLayout: `import React from "react";
592
+ import { Slot } from "expo-router";
593
+ import { SafeAreaProvider } from "react-native-safe-area-context";
594
+ import { AuthProvider } from "../context/AuthContext";
595
+
596
+ export default function RootLayout() {
597
+ return (
598
+ <SafeAreaProvider>
599
+ <AuthProvider>
600
+ <Slot />
601
+ </AuthProvider>
602
+ </SafeAreaProvider>
603
+ );
604
+ }
605
+ `,
606
+ indexRoute: `import React, { useContext } from "react";
607
+ import { ActivityIndicator, View } from "react-native";
608
+ import { Redirect } from "expo-router";
609
+ import { AuthContext } from "../context/AuthContext";
610
+
611
+ export default function Index() {
612
+ const { token, isHydrated } = useContext(AuthContext);
613
+
614
+ if (!isHydrated) {
615
+ return (
616
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
617
+ <ActivityIndicator />
618
+ </View>
619
+ );
620
+ }
621
+
622
+ return <Redirect href={token ? "/(tabs)" : "/(auth)/login"} />;
623
+ }
624
+ `,
625
+ authLayout: `import React, { useContext } from "react";
626
+ import { ActivityIndicator, View } from "react-native";
627
+ import { Redirect, Stack } from "expo-router";
628
+ import { AuthContext } from "../../context/AuthContext";
629
+
630
+ export default function AuthLayout() {
631
+ const { token, isHydrated } = useContext(AuthContext);
632
+
633
+ if (!isHydrated) {
634
+ return (
635
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
636
+ <ActivityIndicator />
637
+ </View>
638
+ );
639
+ }
640
+
641
+ if (token) return <Redirect href="/(tabs)" />;
642
+
643
+ return (
644
+ <Stack screenOptions={{ headerShown: false }}>
645
+ <Stack.Screen name="login" />
646
+ <Stack.Screen name="register" />
647
+ </Stack>
648
+ );
649
+ }
650
+ `,
651
+ tabsLayout: `import React, { useContext } from "react";
652
+ import { ActivityIndicator, View } from "react-native";
653
+ import { Redirect, Tabs } from "expo-router";
654
+ import { AuthContext } from "../../context/AuthContext";
655
+
656
+ export default function TabsLayout() {
657
+ const { token, isHydrated } = useContext(AuthContext);
658
+
659
+ if (!isHydrated) {
660
+ return (
661
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
662
+ <ActivityIndicator />
663
+ </View>
664
+ );
665
+ }
666
+
667
+ if (!token) return <Redirect href="/(auth)/login" />;
668
+
669
+ return (
670
+ <Tabs screenOptions={{ headerShown: false }}>
671
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
672
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
673
+ <Tabs.Screen name="settings" options={{ title: "Settings" }} />
674
+ </Tabs>
675
+ );
676
+ }
677
+ `,
678
+ };
679
+ }
680
+ function getExpoAuthRouteFiles(state) {
681
+ const registerRedux = `import React, { useState } from "react";
682
+ import { Button, Text, TextInput } from "react-native";
683
+ import { SafeAreaView } from "react-native-safe-area-context";
684
+ import { useDispatch } from "react-redux";
685
+ import { registerApi } from "../../api";
686
+ import { login } from "../../store/authSlice";
687
+
688
+ export default function RegisterScreen() {
689
+ const dispatch = useDispatch();
690
+ const [email, setEmail] = useState("");
691
+ const [password, setPassword] = useState("");
692
+ const [error, setError] = useState("");
693
+
694
+ const handleRegister = async () => {
695
+ try {
696
+ setError("");
697
+ const token = await registerApi(email, password);
698
+ dispatch(login(token));
699
+ } catch (err) {
700
+ setError(err instanceof Error ? err.message : "Registration failed");
701
+ }
702
+ };
703
+
704
+ return (
705
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
706
+ <TextInput
707
+ placeholder="Email"
708
+ value={email}
709
+ onChangeText={setEmail}
710
+ autoCapitalize="none"
711
+ autoCorrect={false}
712
+ keyboardType="email-address"
713
+ placeholderTextColor="#888"
714
+ style={{
715
+ borderWidth: 1,
716
+ borderColor: "#ccc",
717
+ borderRadius: 8,
718
+ paddingHorizontal: 12,
719
+ paddingVertical: 10,
720
+ color: "#111",
721
+ marginBottom: 12,
722
+ }}
723
+ />
724
+ <TextInput
725
+ placeholder="Password"
726
+ value={password}
727
+ onChangeText={setPassword}
728
+ secureTextEntry
729
+ placeholderTextColor="#888"
730
+ style={{
731
+ borderWidth: 1,
732
+ borderColor: "#ccc",
733
+ borderRadius: 8,
734
+ paddingHorizontal: 12,
735
+ paddingVertical: 10,
736
+ color: "#111",
737
+ marginBottom: 12,
738
+ }}
739
+ />
740
+ {!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
741
+ <Button title="Register" onPress={() => void handleRegister()} />
742
+ </SafeAreaView>
743
+ );
744
+ }
745
+ `;
746
+ const registerZustand = `import React, { useState } from "react";
747
+ import { Button, Text, TextInput } from "react-native";
748
+ import { SafeAreaView } from "react-native-safe-area-context";
749
+ import { registerApi } from "../../api";
750
+ import { useAuthStore } from "../../store/authStore";
751
+
752
+ export default function RegisterScreen() {
753
+ const login = useAuthStore((state) => state.login);
754
+ const [email, setEmail] = useState("");
755
+ const [password, setPassword] = useState("");
756
+ const [error, setError] = useState("");
757
+
758
+ const handleRegister = async () => {
759
+ try {
760
+ setError("");
761
+ const token = await registerApi(email, password);
762
+ await login(token);
763
+ } catch (err) {
764
+ setError(err instanceof Error ? err.message : "Registration failed");
765
+ }
766
+ };
767
+
768
+ return (
769
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
770
+ <TextInput
771
+ placeholder="Email"
772
+ value={email}
773
+ onChangeText={setEmail}
774
+ autoCapitalize="none"
775
+ autoCorrect={false}
776
+ keyboardType="email-address"
777
+ placeholderTextColor="#888"
778
+ style={{
779
+ borderWidth: 1,
780
+ borderColor: "#ccc",
781
+ borderRadius: 8,
782
+ paddingHorizontal: 12,
783
+ paddingVertical: 10,
784
+ color: "#111",
785
+ marginBottom: 12,
786
+ }}
787
+ />
788
+ <TextInput
789
+ placeholder="Password"
790
+ value={password}
791
+ onChangeText={setPassword}
792
+ secureTextEntry
793
+ placeholderTextColor="#888"
794
+ style={{
795
+ borderWidth: 1,
796
+ borderColor: "#ccc",
797
+ borderRadius: 8,
798
+ paddingHorizontal: 12,
799
+ paddingVertical: 10,
800
+ color: "#111",
801
+ marginBottom: 12,
802
+ }}
803
+ />
804
+ {!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
805
+ <Button title="Register" onPress={() => void handleRegister()} />
806
+ </SafeAreaView>
807
+ );
808
+ }
809
+ `;
810
+ const registerContext = `import React, { useContext, useState } from "react";
811
+ import { Button, Text, TextInput } from "react-native";
812
+ import { SafeAreaView } from "react-native-safe-area-context";
813
+ import { registerApi } from "../../api";
814
+ import { AuthContext } from "../../context/AuthContext";
815
+
816
+ export default function RegisterScreen() {
817
+ const { login } = useContext(AuthContext);
818
+ const [email, setEmail] = useState("");
819
+ const [password, setPassword] = useState("");
820
+ const [error, setError] = useState("");
821
+
822
+ const handleRegister = async () => {
823
+ try {
824
+ setError("");
825
+ const token = await registerApi(email, password);
826
+ await login(token);
827
+ } catch (err) {
828
+ setError(err instanceof Error ? err.message : "Registration failed");
829
+ }
830
+ };
831
+
832
+ return (
833
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
834
+ <TextInput
835
+ placeholder="Email"
836
+ value={email}
837
+ onChangeText={setEmail}
838
+ autoCapitalize="none"
839
+ autoCorrect={false}
840
+ keyboardType="email-address"
841
+ placeholderTextColor="#888"
842
+ style={{
843
+ borderWidth: 1,
844
+ borderColor: "#ccc",
845
+ borderRadius: 8,
846
+ paddingHorizontal: 12,
847
+ paddingVertical: 10,
848
+ color: "#111",
849
+ marginBottom: 12,
850
+ }}
851
+ />
852
+ <TextInput
853
+ placeholder="Password"
854
+ value={password}
855
+ onChangeText={setPassword}
856
+ secureTextEntry
857
+ placeholderTextColor="#888"
858
+ style={{
859
+ borderWidth: 1,
860
+ borderColor: "#ccc",
861
+ borderRadius: 8,
862
+ paddingHorizontal: 12,
863
+ paddingVertical: 10,
864
+ color: "#111",
865
+ marginBottom: 12,
866
+ }}
867
+ />
868
+ {!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
869
+ <Button title="Register" onPress={() => void handleRegister()} />
870
+ </SafeAreaView>
871
+ );
872
+ }
873
+ `;
874
+ const profile = `import React from "react";
875
+ import { Text } from "react-native";
876
+ import { SafeAreaView } from "react-native-safe-area-context";
877
+
878
+ export default function ProfileScreen() {
879
+ return (
880
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
881
+ <Text>ProfileScreen</Text>
882
+ </SafeAreaView>
883
+ );
884
+ }
885
+ `;
886
+ const settings = `import React from "react";
887
+ import { Text } from "react-native";
888
+ import { SafeAreaView } from "react-native-safe-area-context";
889
+
890
+ export default function SettingsScreen() {
891
+ return (
892
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
893
+ <Text>SettingsScreen</Text>
894
+ </SafeAreaView>
895
+ );
896
+ }
897
+ `;
898
+ if (state === "Redux Toolkit") {
899
+ return {
900
+ login: `import React, { useState } from "react";
901
+ import { Button, Text, TextInput } from "react-native";
902
+ import { SafeAreaView } from "react-native-safe-area-context";
903
+ import { useDispatch } from "react-redux";
904
+ import { loginApi } from "../../api";
905
+ import { login } from "../../store/authSlice";
906
+
907
+ export default function LoginScreen() {
908
+ const dispatch = useDispatch();
909
+ const [email, setEmail] = useState("");
910
+ const [password, setPassword] = useState("");
911
+ const [error, setError] = useState("");
912
+
913
+ const handleLogin = async () => {
914
+ try {
915
+ setError("");
916
+ const token = await loginApi(email, password);
917
+ dispatch(login(token));
918
+ } catch (err) {
919
+ setError(err instanceof Error ? err.message : "Login failed");
920
+ }
921
+ };
922
+
923
+ return (
924
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
925
+ <TextInput
926
+ placeholder="Email"
927
+ value={email}
928
+ onChangeText={setEmail}
929
+ autoCapitalize="none"
930
+ autoCorrect={false}
931
+ keyboardType="email-address"
932
+ placeholderTextColor="#888"
933
+ style={{
934
+ borderWidth: 1,
935
+ borderColor: "#ccc",
936
+ borderRadius: 8,
937
+ paddingHorizontal: 12,
938
+ paddingVertical: 10,
939
+ color: "#111",
940
+ marginBottom: 12,
941
+ }}
942
+ />
943
+ <TextInput
944
+ placeholder="Password"
945
+ value={password}
946
+ onChangeText={setPassword}
947
+ secureTextEntry
948
+ placeholderTextColor="#888"
949
+ style={{
950
+ borderWidth: 1,
951
+ borderColor: "#ccc",
952
+ borderRadius: 8,
953
+ paddingHorizontal: 12,
954
+ paddingVertical: 10,
955
+ color: "#111",
956
+ marginBottom: 12,
957
+ }}
958
+ />
959
+ {!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
960
+ <Button title="Login" onPress={() => void handleLogin()} />
961
+ </SafeAreaView>
962
+ );
963
+ }
964
+ `,
965
+ register: registerRedux,
966
+ home: `import React from "react";
967
+ import { Button, Text } from "react-native";
968
+ import { SafeAreaView } from "react-native-safe-area-context";
969
+ import { useDispatch } from "react-redux";
970
+ import { logout } from "../../store/authSlice";
971
+
972
+ export default function HomeScreen() {
973
+ const dispatch = useDispatch();
974
+
975
+ return (
976
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
977
+ <Text>Welcome Home!</Text>
978
+ <Button title="Logout" onPress={() => dispatch(logout())} />
979
+ </SafeAreaView>
980
+ );
981
+ }
982
+ `,
983
+ profile,
984
+ settings,
985
+ };
986
+ }
987
+ if (state === "Zustand") {
988
+ return {
989
+ login: `import React, { useState } from "react";
990
+ import { Button, Text, TextInput } from "react-native";
991
+ import { SafeAreaView } from "react-native-safe-area-context";
992
+ import { loginApi } from "../../api";
993
+ import { useAuthStore } from "../../store/authStore";
994
+
995
+ export default function LoginScreen() {
996
+ const login = useAuthStore((state) => state.login);
997
+ const [email, setEmail] = useState("");
998
+ const [password, setPassword] = useState("");
999
+ const [error, setError] = useState("");
1000
+
1001
+ const handleLogin = async () => {
1002
+ try {
1003
+ setError("");
1004
+ const token = await loginApi(email, password);
1005
+ await login(token);
1006
+ } catch (err) {
1007
+ setError(err instanceof Error ? err.message : "Login failed");
1008
+ }
1009
+ };
1010
+
1011
+ return (
1012
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
1013
+ <TextInput
1014
+ placeholder="Email"
1015
+ value={email}
1016
+ onChangeText={setEmail}
1017
+ autoCapitalize="none"
1018
+ autoCorrect={false}
1019
+ keyboardType="email-address"
1020
+ placeholderTextColor="#888"
1021
+ style={{
1022
+ borderWidth: 1,
1023
+ borderColor: "#ccc",
1024
+ borderRadius: 8,
1025
+ paddingHorizontal: 12,
1026
+ paddingVertical: 10,
1027
+ color: "#111",
1028
+ marginBottom: 12,
1029
+ }}
1030
+ />
1031
+ <TextInput
1032
+ placeholder="Password"
1033
+ value={password}
1034
+ onChangeText={setPassword}
1035
+ secureTextEntry
1036
+ placeholderTextColor="#888"
1037
+ style={{
1038
+ borderWidth: 1,
1039
+ borderColor: "#ccc",
1040
+ borderRadius: 8,
1041
+ paddingHorizontal: 12,
1042
+ paddingVertical: 10,
1043
+ color: "#111",
1044
+ marginBottom: 12,
1045
+ }}
1046
+ />
1047
+ {!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
1048
+ <Button title="Login" onPress={() => void handleLogin()} />
1049
+ </SafeAreaView>
1050
+ );
1051
+ }
1052
+ `,
1053
+ register: registerZustand,
1054
+ home: `import React from "react";
1055
+ import { Button, Text } from "react-native";
1056
+ import { SafeAreaView } from "react-native-safe-area-context";
1057
+ import { useAuthStore } from "../../store/authStore";
1058
+
1059
+ export default function HomeScreen() {
1060
+ const logout = useAuthStore((state) => state.logout);
1061
+
1062
+ return (
1063
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
1064
+ <Text>Welcome Home!</Text>
1065
+ <Button title="Logout" onPress={() => void logout()} />
1066
+ </SafeAreaView>
1067
+ );
1068
+ }
1069
+ `,
1070
+ profile,
1071
+ settings,
1072
+ };
1073
+ }
1074
+ return {
1075
+ login: `import React, { useContext, useState } from "react";
1076
+ import { Button, Text, TextInput } from "react-native";
1077
+ import { SafeAreaView } from "react-native-safe-area-context";
1078
+ import { loginApi } from "../../api";
1079
+ import { AuthContext } from "../../context/AuthContext";
1080
+
1081
+ export default function LoginScreen() {
1082
+ const { login } = useContext(AuthContext);
1083
+ const [email, setEmail] = useState("");
1084
+ const [password, setPassword] = useState("");
1085
+ const [error, setError] = useState("");
1086
+
1087
+ const handleLogin = async () => {
1088
+ try {
1089
+ setError("");
1090
+ const token = await loginApi(email, password);
1091
+ await login(token);
1092
+ } catch (err) {
1093
+ setError(err instanceof Error ? err.message : "Login failed");
1094
+ }
1095
+ };
1096
+
1097
+ return (
1098
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
1099
+ <TextInput
1100
+ placeholder="Email"
1101
+ value={email}
1102
+ onChangeText={setEmail}
1103
+ autoCapitalize="none"
1104
+ autoCorrect={false}
1105
+ keyboardType="email-address"
1106
+ placeholderTextColor="#888"
1107
+ style={{
1108
+ borderWidth: 1,
1109
+ borderColor: "#ccc",
1110
+ borderRadius: 8,
1111
+ paddingHorizontal: 12,
1112
+ paddingVertical: 10,
1113
+ color: "#111",
1114
+ marginBottom: 12,
1115
+ }}
1116
+ />
1117
+ <TextInput
1118
+ placeholder="Password"
1119
+ value={password}
1120
+ onChangeText={setPassword}
1121
+ secureTextEntry
1122
+ placeholderTextColor="#888"
1123
+ style={{
1124
+ borderWidth: 1,
1125
+ borderColor: "#ccc",
1126
+ borderRadius: 8,
1127
+ paddingHorizontal: 12,
1128
+ paddingVertical: 10,
1129
+ color: "#111",
1130
+ marginBottom: 12,
1131
+ }}
1132
+ />
1133
+ {!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
1134
+ <Button title="Login" onPress={() => void handleLogin()} />
1135
+ </SafeAreaView>
1136
+ );
1137
+ }
1138
+ `,
1139
+ register: registerContext,
1140
+ home: `import React, { useContext } from "react";
1141
+ import { Button, Text } from "react-native";
1142
+ import { SafeAreaView } from "react-native-safe-area-context";
1143
+ import { AuthContext } from "../../context/AuthContext";
1144
+
1145
+ export default function HomeScreen() {
1146
+ const { logout } = useContext(AuthContext);
1147
+
1148
+ return (
1149
+ <SafeAreaView style={{ flex: 1, padding: 20 }}>
1150
+ <Text>Welcome Home!</Text>
1151
+ <Button title="Logout" onPress={() => void logout()} />
1152
+ </SafeAreaView>
1153
+ );
1154
+ }
1155
+ `,
1156
+ profile,
1157
+ settings,
1158
+ };
1159
+ }
1160
+ async function writeAuthAppShell(targetPath, state) {
1161
+ const appPath = path_1.default.join(targetPath, "App.tsx");
1162
+ const byState = {
1163
+ "Context API": `import React from "react";
1164
+ import { NavigationContainer } from "@react-navigation/native";
1165
+ import { SafeAreaProvider } from "react-native-safe-area-context";
1166
+ import { AuthProvider } from "./context/AuthContext";
1167
+ import ProtectedStack from "./navigation/ProtectedStack";
1168
+
1169
+ export default function App() {
1170
+ return (
1171
+ <SafeAreaProvider>
1172
+ <AuthProvider>
1173
+ <NavigationContainer>
1174
+ <ProtectedStack />
1175
+ </NavigationContainer>
1176
+ </AuthProvider>
1177
+ </SafeAreaProvider>
1178
+ );
1179
+ }
1180
+ `,
1181
+ "Redux Toolkit": `import React from "react";
1182
+ import { NavigationContainer } from "@react-navigation/native";
1183
+ import { Provider } from "react-redux";
1184
+ import { SafeAreaProvider } from "react-native-safe-area-context";
1185
+ import ProtectedStack from "./navigation/ProtectedStack";
1186
+ import { store } from "./store/store";
1187
+
1188
+ export default function App() {
1189
+ return (
1190
+ <SafeAreaProvider>
1191
+ <Provider store={store}>
1192
+ <NavigationContainer>
1193
+ <ProtectedStack />
1194
+ </NavigationContainer>
1195
+ </Provider>
1196
+ </SafeAreaProvider>
1197
+ );
1198
+ }
1199
+ `,
1200
+ Zustand: `import React from "react";
1201
+ import { NavigationContainer } from "@react-navigation/native";
1202
+ import { SafeAreaProvider } from "react-native-safe-area-context";
1203
+ import ProtectedStack from "./navigation/ProtectedStack";
1204
+
1205
+ export default function App() {
1206
+ return (
1207
+ <SafeAreaProvider>
1208
+ <NavigationContainer>
1209
+ <ProtectedStack />
1210
+ </NavigationContainer>
1211
+ </SafeAreaProvider>
1212
+ );
1213
+ }
1214
+ `,
1215
+ };
1216
+ const content = byState[state];
1217
+ if (!content)
1218
+ return;
1219
+ await fs_extra_1.default.writeFile(appPath, content, "utf8");
1220
+ }
309
1221
  async function configureTsconfigAlias(targetPath, absoluteImports) {
310
1222
  const tsconfigPath = path_1.default.join(targetPath, "tsconfig.json");
311
1223
  if (!(await fs_extra_1.default.pathExists(tsconfigPath)))
@@ -375,7 +1287,9 @@ async function writeIfMissing(filePath, content) {
375
1287
  async function resolveTemplateRoot() {
376
1288
  const candidates = [
377
1289
  path_1.default.join(__dirname, "../templates"), // ts-node: src/generators -> src/templates
378
- path_1.default.join(__dirname, "../../templates"), // built output layouts
1290
+ path_1.default.join(__dirname, "../../templates"), // dist/src/generators -> dist/templates
1291
+ path_1.default.join(__dirname, "../../../src/templates"), // fallback for packaged source templates
1292
+ path_1.default.join(process.cwd(), "src/templates"), // local fallback during development
379
1293
  ];
380
1294
  for (const candidate of candidates) {
381
1295
  if (await fs_extra_1.default.pathExists(candidate))
@@ -383,18 +1297,20 @@ async function resolveTemplateRoot() {
383
1297
  }
384
1298
  throw new Error("Template root not found.");
385
1299
  }
386
- function shouldCopyPath(src) {
387
- const normalized = src.replace(/\\/g, "/");
1300
+ function shouldCopyPath(src, templatePath) {
1301
+ const relative = path_1.default.relative(templatePath, src).replace(/\\/g, "/");
1302
+ if (!relative || relative === ".")
1303
+ return true;
388
1304
  const blockedSegments = [
389
- "/.git/",
390
- "/node_modules/",
391
- "/ios/Pods/",
392
- "/android/.gradle/",
393
- "/vendor/bundle/",
1305
+ ".git",
1306
+ "node_modules",
1307
+ "ios/Pods",
1308
+ "android/.gradle",
1309
+ "vendor/bundle",
394
1310
  ];
395
- if (normalized.endsWith("/.git"))
396
- return false;
397
- return !blockedSegments.some((segment) => normalized.includes(segment));
1311
+ return !blockedSegments.some((segment) => relative === segment ||
1312
+ relative.startsWith(`${segment}/`) ||
1313
+ relative.includes(`/${segment}/`));
398
1314
  }
399
1315
  function isTextFile(filePath) {
400
1316
  const extension = path_1.default.extname(filePath).toLowerCase();