@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.
- package/dist/bin/create-rnstarterkit.js +21 -7
- package/dist/src/generators/appGenerator.js +976 -60
- package/dist/templates/cli-base/.bundle/config +2 -0
- package/dist/templates/cli-base/.eslintrc.js +4 -0
- package/dist/templates/cli-base/.prettierrc.js +5 -0
- package/dist/templates/cli-base/.watchmanconfig +1 -0
- package/dist/templates/cli-base/App.tsx +45 -0
- package/dist/templates/cli-base/Gemfile +16 -0
- package/dist/templates/cli-base/Gemfile.lock +169 -0
- package/dist/templates/cli-base/README.md +97 -0
- package/dist/templates/cli-base/__tests__/App.test.tsx +13 -0
- package/dist/templates/cli-base/android/app/build.gradle +119 -0
- package/dist/templates/cli-base/android/app/debug.keystore +0 -0
- package/dist/templates/cli-base/android/app/proguard-rules.pro +10 -0
- package/dist/templates/cli-base/android/app/src/main/AndroidManifest.xml +27 -0
- package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainActivity.kt +22 -0
- package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainApplication.kt +27 -0
- package/dist/templates/cli-base/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/dist/templates/cli-base/android/app/src/main/res/values/strings.xml +3 -0
- package/dist/templates/cli-base/android/app/src/main/res/values/styles.xml +9 -0
- package/dist/templates/cli-base/android/build.gradle +21 -0
- package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/dist/templates/cli-base/android/gradle.properties +44 -0
- package/dist/templates/cli-base/android/gradlew +251 -0
- package/dist/templates/cli-base/android/gradlew.bat +99 -0
- package/dist/templates/cli-base/android/settings.gradle +6 -0
- package/dist/templates/cli-base/app.json +4 -0
- package/dist/templates/cli-base/babel.config.js +3 -0
- package/dist/templates/cli-base/index.js +9 -0
- package/dist/templates/cli-base/ios/.xcode.env +11 -0
- package/dist/templates/cli-base/ios/BaseApp/AppDelegate.swift +48 -0
- package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/Contents.json +6 -0
- package/dist/templates/cli-base/ios/BaseApp/Info.plist +60 -0
- package/dist/templates/cli-base/ios/BaseApp/LaunchScreen.storyboard +47 -0
- package/dist/templates/cli-base/ios/BaseApp/PrivacyInfo.xcprivacy +37 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +494 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/xcshareddata/xcschemes/BaseApp.xcscheme +88 -0
- package/dist/templates/cli-base/ios/BaseApp.xcworkspace/contents.xcworkspacedata +10 -0
- package/dist/templates/cli-base/ios/Podfile +34 -0
- package/dist/templates/cli-base/ios/Podfile.lock +2165 -0
- package/dist/templates/cli-base/jest.config.js +3 -0
- package/dist/templates/cli-base/metro.config.js +11 -0
- package/dist/templates/cli-base/package-lock.json +11859 -0
- package/dist/templates/cli-base/package.json +41 -0
- package/dist/templates/cli-base/tsconfig.json +8 -0
- package/dist/templates/expo-base/.vscode/extensions.json +1 -0
- package/dist/templates/expo-base/.vscode/settings.json +7 -0
- package/dist/templates/expo-base/README.md +50 -0
- package/dist/templates/expo-base/app/_layout.tsx +12 -0
- package/dist/templates/expo-base/app/index.tsx +9 -0
- package/dist/templates/expo-base/app.json +48 -0
- package/dist/templates/expo-base/assets/images/android-icon-background.png +0 -0
- package/dist/templates/expo-base/assets/images/android-icon-foreground.png +0 -0
- package/dist/templates/expo-base/assets/images/android-icon-monochrome.png +0 -0
- package/dist/templates/expo-base/assets/images/favicon.png +0 -0
- package/dist/templates/expo-base/assets/images/icon.png +0 -0
- package/dist/templates/expo-base/assets/images/partial-react-logo.png +0 -0
- package/dist/templates/expo-base/assets/images/react-logo.png +0 -0
- package/dist/templates/expo-base/assets/images/react-logo@2x.png +0 -0
- package/dist/templates/expo-base/assets/images/react-logo@3x.png +0 -0
- package/dist/templates/expo-base/assets/images/splash-icon.png +0 -0
- package/dist/templates/expo-base/components/ui/collapsible.tsx +45 -0
- package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +32 -0
- package/dist/templates/expo-base/components/ui/icon-symbol.tsx +41 -0
- package/dist/templates/expo-base/eslint.config.js +10 -0
- package/dist/templates/expo-base/package-lock.json +12916 -0
- package/dist/templates/expo-base/package.json +46 -0
- package/dist/templates/expo-base/tsconfig.json +17 -0
- package/dist/templates/optional/apiClient/api/client.ts +142 -0
- package/dist/templates/optional/apiClient/api/index.ts +1 -0
- package/dist/templates/optional/apiClient/config/env.cli.ts +5 -0
- package/dist/templates/optional/apiClient/config/env.expo.ts +4 -0
- package/dist/templates/optional/apiClient/config/env.ts +1 -0
- package/dist/templates/optional/apiClient/env.d.ts +3 -0
- package/dist/templates/optional/auth-context/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-context/context/AuthContext.tsx +47 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +38 -0
- package/dist/templates/optional/auth-context/navigation/cli/BottomTabs.tsx +17 -0
- package/dist/templates/optional/auth-context/navigation/expo/BottomTabs.tsx +29 -0
- package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +15 -0
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +63 -0
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +11 -0
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +63 -0
- package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +11 -0
- package/dist/templates/optional/auth-context/utils/storage.ts +13 -0
- package/dist/templates/optional/auth-redux/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +30 -0
- package/dist/templates/optional/auth-redux/navigation/cli/BottomTabs.tsx +17 -0
- package/dist/templates/optional/auth-redux/navigation/expo/BottomTabs.tsx +31 -0
- package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +16 -0
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +64 -0
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +15 -0
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +64 -0
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +15 -0
- package/dist/templates/optional/auth-redux/store/authSlice.ts +25 -0
- package/dist/templates/optional/auth-redux/store/store.ts +11 -0
- package/dist/templates/optional/auth-zustand/api/endpoints/auth.ts +14 -0
- package/dist/templates/optional/auth-zustand/navigation/BottomTabs.tsx +1 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +44 -0
- package/dist/templates/optional/auth-zustand/navigation/cli/BottomTabs.tsx +17 -0
- package/dist/templates/optional/auth-zustand/navigation/expo/BottomTabs.tsx +31 -0
- package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +15 -0
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +63 -0
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +11 -0
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +63 -0
- package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +11 -0
- package/dist/templates/optional/auth-zustand/store/authStore.ts +30 -0
- package/dist/templates/optional/auth-zustand/utils/storage.ts +13 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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"), //
|
|
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
|
|
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
|
-
"
|
|
390
|
-
"
|
|
391
|
-
"
|
|
392
|
-
"
|
|
393
|
-
"
|
|
1305
|
+
".git",
|
|
1306
|
+
"node_modules",
|
|
1307
|
+
"ios/Pods",
|
|
1308
|
+
"android/.gradle",
|
|
1309
|
+
"vendor/bundle",
|
|
394
1310
|
];
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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();
|