@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.
- package/dist/src/generators/appGenerator.js +95 -93
- package/dist/templates/expo-base/App.tsx +32 -0
- package/dist/templates/expo-base/app.json +0 -2
- package/dist/templates/expo-base/package.json +1 -3
- package/dist/templates/optional/apiClient/api/client.axios.ts +124 -0
- package/dist/templates/optional/apiClient/api/client.ts +59 -3
- package/package.json +1 -1
|
@@ -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.
|
|
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");
|
|
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
|
|
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
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
394
|
+
".git",
|
|
395
|
+
"node_modules",
|
|
396
|
+
"ios/Pods",
|
|
397
|
+
"android/.gradle",
|
|
398
|
+
"vendor/bundle",
|
|
397
399
|
];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
|
-
"main": "expo
|
|
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
|
-
|
|
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