@highbeek/create-rnstarterkit 1.0.2-beta.3 → 1.0.2-beta.5

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.
@@ -15,7 +15,7 @@ async function generateApp(options) {
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
21
  await createStandardStructure(targetPath);
@@ -30,6 +30,7 @@ async function generateApp(options) {
30
30
  authFolder === "auth-zustand" ||
31
31
  authFolder === "auth-redux") {
32
32
  await setAuthTabsByPlatform(targetPath, platform);
33
+ await writeAuthAppShell(targetPath, state);
33
34
  }
34
35
  }
35
36
  if (apiClient) {
@@ -44,6 +45,11 @@ async function generateApp(options) {
44
45
  await copyOptionalModule("react-query", targetPath);
45
46
  if (storage === "MMKV")
46
47
  await copyOptionalModule("mmkv", targetPath);
48
+ await configureStateAndAuthDependencies(targetPath, {
49
+ state,
50
+ auth,
51
+ storage,
52
+ });
47
53
  await configureAbsoluteImports(targetPath, platform, absoluteImports);
48
54
  await configureDataFetching(targetPath, dataFetching);
49
55
  await configureApiClientTransport(targetPath, apiClient, apiClientType);
@@ -198,6 +204,7 @@ export const queryClient = new QueryClient();
198
204
  }
199
205
  async function configureApiClientTransport(targetPath, apiClient, apiClientType) {
200
206
  if (apiClient && apiClientType === "Axios") {
207
+ const templateRoot = await resolveTemplateRoot();
201
208
  await ensureDependencies(targetPath, {
202
209
  dependencies: {
203
210
  axios: "^1.12.2",
@@ -211,88 +218,9 @@ export const axiosClient = axios.create({
211
218
  timeout: 15000,
212
219
  });
213
220
  `);
214
- await fs_extra_1.default.writeFile(path_1.default.join(targetPath, "api/client.ts"), `import axios from "axios";
215
- import { API_BASE_URL } from "../config/env";
216
-
217
- export class ApiError extends Error {
218
- status: number;
219
- data: unknown;
220
-
221
- constructor(status: number, message: string, data: unknown) {
222
- super(message);
223
- this.name = "ApiError";
224
- this.status = status;
225
- this.data = data;
226
- }
227
- }
228
-
229
- type RequestOptions = {
230
- headers?: Record<string, string>;
231
- query?: Record<string, string | number | boolean | undefined>;
232
- token?: string | null;
233
- signal?: AbortSignal;
234
- };
235
-
236
- const client = axios.create({
237
- baseURL: API_BASE_URL,
238
- timeout: 15000,
239
- });
240
-
241
- function resolveErrorMessage(data: unknown, status: number) {
242
- if (
243
- typeof data === "object" &&
244
- data !== null &&
245
- "message" in data &&
246
- typeof (data as { message: unknown }).message === "string"
247
- ) {
248
- return (data as { message: string }).message;
249
- }
250
- return \`Request failed with status \${status}\`;
251
- }
252
-
253
- async function request<T>(
254
- method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
255
- path: string,
256
- body?: unknown,
257
- options: RequestOptions = {},
258
- ): Promise<T> {
259
- const { headers = {}, query, token, signal } = options;
260
- try {
261
- const response = await client.request<T>({
262
- method,
263
- url: path,
264
- data: body,
265
- params: query,
266
- signal,
267
- headers: {
268
- ...(token ? { Authorization: \`Bearer \${token}\` } : {}),
269
- ...headers,
270
- },
271
- });
272
- return response.data;
273
- } catch (error) {
274
- if (axios.isAxiosError(error)) {
275
- const status = error.response?.status ?? 500;
276
- const data = error.response?.data ?? null;
277
- throw new ApiError(status, resolveErrorMessage(data, status), data);
278
- }
279
- throw error;
280
- }
281
- }
282
-
283
- export const apiClient = {
284
- get: <T>(path: string, options?: RequestOptions) =>
285
- request<T>("GET", path, undefined, options),
286
- post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
287
- request<T>("POST", path, body, options),
288
- put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
289
- request<T>("PUT", path, body, options),
290
- patch: <T>(path: string, body?: unknown, options?: RequestOptions) =>
291
- request<T>("PATCH", path, body, options),
292
- delete: <T>(path: string, options?: RequestOptions) =>
293
- request<T>("DELETE", path, undefined, options),
294
- };
295
- `, "utf8");
221
+ await fs_extra_1.default.copy(path_1.default.join(templateRoot, "optional/apiClient/api/client.axios.ts"), path_1.default.join(targetPath, "api/client.ts"), {
222
+ overwrite: true,
223
+ });
296
224
  }
297
225
  }
298
226
  async function configureValidation(targetPath, validation) {
@@ -307,6 +235,78 @@ async function configureValidation(targetPath, validation) {
307
235
  await ensureDependencies(targetPath, { dependencies });
308
236
  }
309
237
  }
238
+ async function configureStateAndAuthDependencies(targetPath, options) {
239
+ const dependencies = {};
240
+ if (options.auth) {
241
+ dependencies["@react-navigation/native-stack"] = "^7.3.29";
242
+ }
243
+ if (options.state === "Redux Toolkit") {
244
+ dependencies["@reduxjs/toolkit"] = "^2.9.0";
245
+ dependencies["react-redux"] = "^9.2.0";
246
+ }
247
+ if (options.state === "Zustand") {
248
+ dependencies.zustand = "^5.0.8";
249
+ }
250
+ if (options.storage === "AsyncStorage" &&
251
+ (options.state === "Context API" || options.state === "Zustand")) {
252
+ dependencies["@react-native-async-storage/async-storage"] = "^2.2.0";
253
+ }
254
+ if (Object.keys(dependencies).length > 0) {
255
+ await ensureDependencies(targetPath, { dependencies });
256
+ }
257
+ }
258
+ async function writeAuthAppShell(targetPath, state) {
259
+ const appPath = path_1.default.join(targetPath, "App.tsx");
260
+ const byState = {
261
+ "Context API": `import React from "react";
262
+ import { NavigationContainer } from "@react-navigation/native";
263
+ import { AuthProvider } from "./context/AuthContext";
264
+ import ProtectedStack from "./navigation/ProtectedStack";
265
+
266
+ export default function App() {
267
+ return (
268
+ <AuthProvider>
269
+ <NavigationContainer>
270
+ <ProtectedStack />
271
+ </NavigationContainer>
272
+ </AuthProvider>
273
+ );
274
+ }
275
+ `,
276
+ "Redux Toolkit": `import React from "react";
277
+ import { NavigationContainer } from "@react-navigation/native";
278
+ import { Provider } from "react-redux";
279
+ import ProtectedStack from "./navigation/ProtectedStack";
280
+ import { store } from "./store/store";
281
+
282
+ export default function App() {
283
+ return (
284
+ <Provider store={store}>
285
+ <NavigationContainer>
286
+ <ProtectedStack />
287
+ </NavigationContainer>
288
+ </Provider>
289
+ );
290
+ }
291
+ `,
292
+ Zustand: `import React from "react";
293
+ import { NavigationContainer } from "@react-navigation/native";
294
+ import ProtectedStack from "./navigation/ProtectedStack";
295
+
296
+ export default function App() {
297
+ return (
298
+ <NavigationContainer>
299
+ <ProtectedStack />
300
+ </NavigationContainer>
301
+ );
302
+ }
303
+ `,
304
+ };
305
+ const content = byState[state];
306
+ if (!content)
307
+ return;
308
+ await fs_extra_1.default.writeFile(appPath, content, "utf8");
309
+ }
310
310
  async function configureTsconfigAlias(targetPath, absoluteImports) {
311
311
  const tsconfigPath = path_1.default.join(targetPath, "tsconfig.json");
312
312
  if (!(await fs_extra_1.default.pathExists(tsconfigPath)))
@@ -386,18 +386,20 @@ async function resolveTemplateRoot() {
386
386
  }
387
387
  throw new Error("Template root not found.");
388
388
  }
389
- function shouldCopyPath(src) {
390
- const normalized = src.replace(/\\/g, "/");
389
+ function shouldCopyPath(src, templatePath) {
390
+ const relative = path_1.default.relative(templatePath, src).replace(/\\/g, "/");
391
+ if (!relative || relative === ".")
392
+ return true;
391
393
  const blockedSegments = [
392
- "/.git/",
393
- "/node_modules/",
394
- "/ios/Pods/",
395
- "/android/.gradle/",
396
- "/vendor/bundle/",
394
+ ".git",
395
+ "node_modules",
396
+ "ios/Pods",
397
+ "android/.gradle",
398
+ "vendor/bundle",
397
399
  ];
398
- if (normalized.endsWith("/.git"))
399
- return false;
400
- return !blockedSegments.some((segment) => normalized.includes(segment));
400
+ return !blockedSegments.some((segment) => relative === segment ||
401
+ relative.startsWith(`${segment}/`) ||
402
+ relative.includes(`/${segment}/`));
401
403
  }
402
404
  function isTextFile(filePath) {
403
405
  const extension = path_1.default.extname(filePath).toLowerCase();
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { SafeAreaView, StyleSheet, Text } from "react-native";
3
+
4
+ export default function App() {
5
+ return (
6
+ <SafeAreaView style={styles.container}>
7
+ <Text style={styles.title}>RN Starter Kit</Text>
8
+ <Text style={styles.subtitle}>
9
+ Build your app by adding screens and navigation.
10
+ </Text>
11
+ </SafeAreaView>
12
+ );
13
+ }
14
+
15
+ const styles = StyleSheet.create({
16
+ container: {
17
+ flex: 1,
18
+ alignItems: "center",
19
+ justifyContent: "center",
20
+ padding: 24,
21
+ },
22
+ title: {
23
+ fontSize: 24,
24
+ fontWeight: "700",
25
+ marginBottom: 8,
26
+ },
27
+ subtitle: {
28
+ fontSize: 15,
29
+ textAlign: "center",
30
+ color: "#666",
31
+ },
32
+ });
@@ -26,7 +26,6 @@
26
26
  "favicon": "./assets/images/favicon.png"
27
27
  },
28
28
  "plugins": [
29
- "expo-router",
30
29
  [
31
30
  "expo-splash-screen",
32
31
  {
@@ -41,7 +40,6 @@
41
40
  ]
42
41
  ],
43
42
  "experiments": {
44
- "typedRoutes": true,
45
43
  "reactCompiler": true
46
44
  }
47
45
  }
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "{{projectName}}",
3
- "main": "expo-router/entry",
3
+ "main": "node_modules/expo/AppEntry.js",
4
4
  "version": "1.0.0",
5
5
  "scripts": {
6
6
  "start": "expo start",
7
- "reset-project": "node ./scripts/reset-project.js",
8
7
  "android": "expo start --android",
9
8
  "ios": "expo start --ios",
10
9
  "web": "expo start --web",
@@ -21,7 +20,6 @@
21
20
  "expo-haptics": "~15.0.8",
22
21
  "expo-image": "~3.0.11",
23
22
  "expo-linking": "~8.0.11",
24
- "expo-router": "~6.0.23",
25
23
  "expo-splash-screen": "~31.0.13",
26
24
  "expo-status-bar": "~3.0.9",
27
25
  "expo-symbols": "~1.0.8",
@@ -0,0 +1,124 @@
1
+ import axios, {
2
+ AxiosError,
3
+ AxiosRequestConfig,
4
+ AxiosResponse,
5
+ InternalAxiosRequestConfig,
6
+ } from "axios";
7
+ import { API_BASE_URL } from "../config/env";
8
+
9
+ export class ApiError extends Error {
10
+ status: number;
11
+ data: unknown;
12
+
13
+ constructor(status: number, message: string, data: unknown) {
14
+ super(message);
15
+ this.name = "ApiError";
16
+ this.status = status;
17
+ this.data = data;
18
+ }
19
+ }
20
+
21
+ type RequestOptions = {
22
+ headers?: Record<string, string>;
23
+ query?: Record<string, string | number | boolean | undefined>;
24
+ token?: string | null;
25
+ signal?: AbortSignal;
26
+ };
27
+
28
+ const client = axios.create({
29
+ baseURL: API_BASE_URL,
30
+ timeout: 15000,
31
+ });
32
+
33
+ let authTokenGetter: (() => string | null | undefined) | null = null;
34
+
35
+ export function setAuthTokenGetter(getter: (() => string | null | undefined) | null) {
36
+ authTokenGetter = getter;
37
+ }
38
+
39
+ function resolveErrorMessage(data: unknown, status: number) {
40
+ if (
41
+ typeof data === "object" &&
42
+ data !== null &&
43
+ "message" in data &&
44
+ typeof (data as { message: unknown }).message === "string"
45
+ ) {
46
+ return (data as { message: string }).message;
47
+ }
48
+ return `Request failed with status ${status}`;
49
+ }
50
+
51
+ export function addRequestInterceptor(
52
+ interceptor: (
53
+ config: InternalAxiosRequestConfig,
54
+ ) =>
55
+ | InternalAxiosRequestConfig
56
+ | Promise<InternalAxiosRequestConfig>,
57
+ ) {
58
+ return client.interceptors.request.use(interceptor);
59
+ }
60
+
61
+ export function addResponseInterceptor(
62
+ onFulfilled?: (
63
+ response: AxiosResponse,
64
+ ) => AxiosResponse | Promise<AxiosResponse>,
65
+ onRejected?: (error: AxiosError) => unknown,
66
+ ) {
67
+ return client.interceptors.response.use(onFulfilled, onRejected);
68
+ }
69
+
70
+ // Default auth + error interceptors.
71
+ addRequestInterceptor((config) => {
72
+ const token = authTokenGetter?.();
73
+ if (token) {
74
+ config.headers.Authorization = `Bearer ${token}`;
75
+ }
76
+ return config;
77
+ });
78
+
79
+ addResponseInterceptor(
80
+ (response) => response,
81
+ (error) => {
82
+ if (axios.isAxiosError(error)) {
83
+ const status = error.response?.status ?? 500;
84
+ const data = error.response?.data ?? null;
85
+ return Promise.reject(new ApiError(status, resolveErrorMessage(data, status), data));
86
+ }
87
+ return Promise.reject(error);
88
+ },
89
+ );
90
+
91
+ async function request<T>(
92
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
93
+ path: string,
94
+ body?: unknown,
95
+ options: RequestOptions = {},
96
+ ): Promise<T> {
97
+ const { headers = {}, query, token, signal } = options;
98
+ const requestConfig: AxiosRequestConfig = {
99
+ method,
100
+ url: path,
101
+ data: body,
102
+ params: query,
103
+ signal,
104
+ headers: {
105
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
106
+ ...headers,
107
+ },
108
+ };
109
+ const response = await client.request<T>(requestConfig);
110
+ return response.data;
111
+ }
112
+
113
+ export const apiClient = {
114
+ get: <T>(path: string, options?: RequestOptions) =>
115
+ request<T>("GET", path, undefined, options),
116
+ post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
117
+ request<T>("POST", path, body, options),
118
+ put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
119
+ request<T>("PUT", path, body, options),
120
+ patch: <T>(path: string, body?: unknown, options?: RequestOptions) =>
121
+ request<T>("PATCH", path, body, options),
122
+ delete: <T>(path: string, options?: RequestOptions) =>
123
+ request<T>("DELETE", path, undefined, options),
124
+ };
@@ -20,6 +20,41 @@ type RequestOptions = {
20
20
  signal?: AbortSignal;
21
21
  };
22
22
 
23
+ type RequestInterceptor = (
24
+ input: {
25
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
26
+ path: string;
27
+ options: RequestOptions;
28
+ },
29
+ ) =>
30
+ | {
31
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
32
+ path: string;
33
+ options: RequestOptions;
34
+ }
35
+ | Promise<{
36
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
37
+ path: string;
38
+ options: RequestOptions;
39
+ }>;
40
+ type ResponseInterceptor = (response: Response) => Response | Promise<Response>;
41
+
42
+ let authTokenGetter: (() => string | null | undefined) | null = null;
43
+ const requestInterceptors: RequestInterceptor[] = [];
44
+ const responseInterceptors: ResponseInterceptor[] = [];
45
+
46
+ export function setAuthTokenGetter(getter: (() => string | null | undefined) | null) {
47
+ authTokenGetter = getter;
48
+ }
49
+
50
+ export function addRequestInterceptor(interceptor: RequestInterceptor) {
51
+ requestInterceptors.push(interceptor);
52
+ }
53
+
54
+ export function addResponseInterceptor(interceptor: ResponseInterceptor) {
55
+ responseInterceptors.push(interceptor);
56
+ }
57
+
23
58
  function buildUrl(path: string, query?: RequestOptions["query"]) {
24
59
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
25
60
  const url = new URL(`${API_BASE_URL}${normalizedPath}`);
@@ -56,10 +91,15 @@ async function request<T>(
56
91
  path: string,
57
92
  options: RequestOptions = {},
58
93
  ): Promise<T> {
59
- const { headers = {}, query, body, token, signal } = options;
94
+ let requestInput = { method, path, options };
95
+ for (const interceptor of requestInterceptors) {
96
+ requestInput = await interceptor(requestInput);
97
+ }
60
98
 
61
- const response = await fetch(buildUrl(path, query), {
62
- method,
99
+ const { headers = {}, query, body, token, signal } = requestInput.options;
100
+
101
+ let response = await fetch(buildUrl(requestInput.path, query), {
102
+ method: requestInput.method,
63
103
  headers: {
64
104
  "Content-Type": "application/json",
65
105
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
@@ -69,6 +109,10 @@ async function request<T>(
69
109
  signal,
70
110
  });
71
111
 
112
+ for (const interceptor of responseInterceptors) {
113
+ response = await interceptor(response);
114
+ }
115
+
72
116
  return parseResponse<T>(response);
73
117
  }
74
118
 
@@ -84,3 +128,15 @@ export const apiClient = {
84
128
  delete: <T>(path: string, options?: Omit<RequestOptions, "body">) =>
85
129
  request<T>("DELETE", path, options),
86
130
  };
131
+
132
+ // Default interceptor setup: attach bearer token if configured.
133
+ addRequestInterceptor((input) => {
134
+ const token = input.options.token ?? authTokenGetter?.() ?? null;
135
+ return {
136
+ ...input,
137
+ options: {
138
+ ...input.options,
139
+ token,
140
+ },
141
+ };
142
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highbeek/create-rnstarterkit",
3
- "version": "1.0.2-beta.3",
3
+ "version": "1.0.2-beta.5",
4
4
  "description": "CLI to scaffold production-ready React Native app structures.",
5
5
  "main": "dist/src/generators/appGenerator.js",
6
6
  "bin": {