@highbeek/create-rnstarterkit 1.0.2-beta.3 → 1.0.2-beta.4
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);
|
|
@@ -198,6 +198,7 @@ export const queryClient = new QueryClient();
|
|
|
198
198
|
}
|
|
199
199
|
async function configureApiClientTransport(targetPath, apiClient, apiClientType) {
|
|
200
200
|
if (apiClient && apiClientType === "Axios") {
|
|
201
|
+
const templateRoot = await resolveTemplateRoot();
|
|
201
202
|
await ensureDependencies(targetPath, {
|
|
202
203
|
dependencies: {
|
|
203
204
|
axios: "^1.12.2",
|
|
@@ -211,88 +212,9 @@ export const axiosClient = axios.create({
|
|
|
211
212
|
timeout: 15000,
|
|
212
213
|
});
|
|
213
214
|
`);
|
|
214
|
-
await fs_extra_1.default.
|
|
215
|
-
|
|
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");
|
|
215
|
+
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"), {
|
|
216
|
+
overwrite: true,
|
|
217
|
+
});
|
|
296
218
|
}
|
|
297
219
|
}
|
|
298
220
|
async function configureValidation(targetPath, validation) {
|
|
@@ -386,18 +308,20 @@ async function resolveTemplateRoot() {
|
|
|
386
308
|
}
|
|
387
309
|
throw new Error("Template root not found.");
|
|
388
310
|
}
|
|
389
|
-
function shouldCopyPath(src) {
|
|
390
|
-
const
|
|
311
|
+
function shouldCopyPath(src, templatePath) {
|
|
312
|
+
const relative = path_1.default.relative(templatePath, src).replace(/\\/g, "/");
|
|
313
|
+
if (!relative || relative === ".")
|
|
314
|
+
return true;
|
|
391
315
|
const blockedSegments = [
|
|
392
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
316
|
+
".git",
|
|
317
|
+
"node_modules",
|
|
318
|
+
"ios/Pods",
|
|
319
|
+
"android/.gradle",
|
|
320
|
+
"vendor/bundle",
|
|
397
321
|
];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
322
|
+
return !blockedSegments.some((segment) => relative === segment ||
|
|
323
|
+
relative.startsWith(`${segment}/`) ||
|
|
324
|
+
relative.includes(`/${segment}/`));
|
|
401
325
|
}
|
|
402
326
|
function isTextFile(filePath) {
|
|
403
327
|
const extension = path_1.default.extname(filePath).toLowerCase();
|
|
@@ -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
|
-
|
|
94
|
+
let requestInput = { method, path, options };
|
|
95
|
+
for (const interceptor of requestInterceptors) {
|
|
96
|
+
requestInput = await interceptor(requestInput);
|
|
97
|
+
}
|
|
60
98
|
|
|
61
|
-
const
|
|
62
|
-
|
|
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