@next-feature/client 0.1.0-MIGRATION.22.0.3
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/.babelrc +12 -0
- package/README.md +7 -0
- package/eslint.config.mjs +12 -0
- package/jest.config.ts +10 -0
- package/package.json +12 -0
- package/project.json +32 -0
- package/src/components/api-error-boundary.tsx +58 -0
- package/src/hooks/use-api-error.tsx +39 -0
- package/src/index.ts +30 -0
- package/src/lib/client.ts +431 -0
- package/src/lib/error.ts +157 -0
- package/src/lib/types/index.ts +13 -0
- package/src/lib/utils/error.ts +136 -0
- package/src/lib/utils/helper.ts +20 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +28 -0
- package/tsconfig.spec.json +22 -0
- package/vite.config.ts +48 -0
package/.babelrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import nx from '@nx/eslint-plugin';
|
|
2
|
+
import baseConfig from '../../eslint.config.mjs';
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
...baseConfig,
|
|
6
|
+
...nx.configs['flat/react'],
|
|
7
|
+
{
|
|
8
|
+
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
9
|
+
// Override or add rules here
|
|
10
|
+
rules: {},
|
|
11
|
+
},
|
|
12
|
+
];
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'client',
|
|
3
|
+
preset: '../../jest.preset.js',
|
|
4
|
+
transform: {
|
|
5
|
+
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
|
6
|
+
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
|
7
|
+
},
|
|
8
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
9
|
+
coverageDirectory: '../../coverage/clients/client',
|
|
10
|
+
};
|
package/package.json
ADDED
package/project.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "client",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "clients/client/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/vite:build",
|
|
10
|
+
"outputs": ["{options.outputPath}"],
|
|
11
|
+
"defaultConfiguration": "production",
|
|
12
|
+
"options": {
|
|
13
|
+
"outputPath": "dist/clients/client"
|
|
14
|
+
},
|
|
15
|
+
"configurations": {
|
|
16
|
+
"development": {
|
|
17
|
+
"mode": "development"
|
|
18
|
+
},
|
|
19
|
+
"production": {
|
|
20
|
+
"mode": "production"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"test": {
|
|
25
|
+
"executor": "@nx/jest:jest",
|
|
26
|
+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
27
|
+
"options": {
|
|
28
|
+
"jestConfig": "clients/client/jest.config.ts"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
4
|
+
import { ApiError } from '../lib/error';
|
|
5
|
+
import { getErrorMessage } from '../lib/utils/error';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
fallback?: (error: ApiError) => ReactNode;
|
|
10
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface State {
|
|
14
|
+
hasError: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ApiErrorBoundary extends Component<Props, State> {
|
|
19
|
+
constructor(props: Props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = { hasError: false, error: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getDerivedStateFromError(error: Error): State {
|
|
25
|
+
return { hasError: true, error };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
29
|
+
console.error('ApiErrorBoundary caught error:', error, errorInfo);
|
|
30
|
+
|
|
31
|
+
if (this.props.onError) {
|
|
32
|
+
this.props.onError(error, errorInfo);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render() {
|
|
37
|
+
if (this.state.hasError && this.state.error) {
|
|
38
|
+
if (this.state.error instanceof ApiError && this.props.fallback) {
|
|
39
|
+
return this.props.fallback(this.state.error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Default error UI
|
|
43
|
+
return (
|
|
44
|
+
<div className="error-container">
|
|
45
|
+
<h2>Something went wrong</h2>
|
|
46
|
+
<p>{getErrorMessage(this.state.error)}</p>
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => this.setState({ hasError: false, error: null })}
|
|
49
|
+
>
|
|
50
|
+
Try again
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this.props.children;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { ApiError } from '../lib/error';
|
|
5
|
+
import { getErrorMessage } from '../lib/utils/error';
|
|
6
|
+
|
|
7
|
+
interface UseApiErrorResult {
|
|
8
|
+
error: ApiError | null;
|
|
9
|
+
setError: (error: ApiError | null) => void;
|
|
10
|
+
clearError: () => void;
|
|
11
|
+
handleError: (error: unknown) => void;
|
|
12
|
+
errorMessage: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useApiError(): UseApiErrorResult {
|
|
16
|
+
const [error, setError] = useState<ApiError | null>(null);
|
|
17
|
+
|
|
18
|
+
const clearError = useCallback(() => {
|
|
19
|
+
setError(null);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const handleError = useCallback((err: unknown) => {
|
|
23
|
+
if (err instanceof ApiError) {
|
|
24
|
+
setError(err);
|
|
25
|
+
} else {
|
|
26
|
+
console.error('Non-API error:', err);
|
|
27
|
+
}
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const errorMessage = error ? getErrorMessage(error) : null;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
error,
|
|
34
|
+
setError,
|
|
35
|
+
clearError,
|
|
36
|
+
handleError,
|
|
37
|
+
errorMessage,
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client package exports
|
|
3
|
+
*
|
|
4
|
+
* This client package provides all necessary API client utilities.
|
|
5
|
+
* All implementations are self-contained and do not depend on external packages.
|
|
6
|
+
*
|
|
7
|
+
* CUSTOMIZATION:
|
|
8
|
+
* You can easily customize any part of the implementation:
|
|
9
|
+
*
|
|
10
|
+
* 1. For custom ApiClient/ApiError logic with interceptors:
|
|
11
|
+
* - Run: npx nx g next-feature:client-config --projectName=<your-project>
|
|
12
|
+
* - This creates lib/client/config.ts for centralized setup
|
|
13
|
+
*
|
|
14
|
+
* 2. For full custom implementations:
|
|
15
|
+
* - Edit src/lib/client.ts to customize ApiClient behavior
|
|
16
|
+
* - Edit src/lib/error.ts to customize error handling
|
|
17
|
+
* - The changes are immediately reflected in all imports
|
|
18
|
+
*
|
|
19
|
+
* 3. For project-specific hooks/components:
|
|
20
|
+
* - Import directly: import { useApiError } from './hooks/use-api-error'
|
|
21
|
+
* - Customize src/hooks/use-api-error.tsx
|
|
22
|
+
* - Customize src/components/api-error-boundary.tsx
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Core API utilities
|
|
26
|
+
export { ApiClient, type ApiClientConfig } from './lib/client';
|
|
27
|
+
export { ApiError, ApiErrorBuilder, type ProblemDetail } from './lib/error';
|
|
28
|
+
export * from './lib/types';
|
|
29
|
+
export * from './lib/utils/error';
|
|
30
|
+
export * from './lib/utils/helper';
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import axios, {
|
|
2
|
+
AxiosError,
|
|
3
|
+
AxiosInstance,
|
|
4
|
+
AxiosRequestConfig,
|
|
5
|
+
AxiosResponse,
|
|
6
|
+
InternalAxiosRequestConfig,
|
|
7
|
+
} from 'axios';
|
|
8
|
+
import { ApiError, ProblemDetail } from './error';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration options for the API client
|
|
12
|
+
*/
|
|
13
|
+
export interface ApiClientConfig {
|
|
14
|
+
baseURL: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
enableRefreshToken?: boolean;
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
retryDelay?: number;
|
|
19
|
+
onAuthenticated?: (
|
|
20
|
+
config: InternalAxiosRequestConfig
|
|
21
|
+
) => void | Promise<void>;
|
|
22
|
+
onUnauthorized?: () => void | Promise<void>;
|
|
23
|
+
onRefreshTokenExpired?: () => void | Promise<void>;
|
|
24
|
+
onRefreshToken?: () => string | Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pending request queue item
|
|
29
|
+
*/
|
|
30
|
+
interface PendingRequest {
|
|
31
|
+
resolve: (token: string) => void;
|
|
32
|
+
reject: (error: any) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Axios wrapper with JWT/Refresh token handling
|
|
37
|
+
*/
|
|
38
|
+
export class ApiClient {
|
|
39
|
+
private readonly instance: AxiosInstance;
|
|
40
|
+
private isRefreshing = false;
|
|
41
|
+
private pendingRequests: PendingRequest[] = [];
|
|
42
|
+
private config: Required<ApiClientConfig>;
|
|
43
|
+
|
|
44
|
+
constructor(config: ApiClientConfig) {
|
|
45
|
+
this.config = {
|
|
46
|
+
timeout: 30000,
|
|
47
|
+
enableRefreshToken: true,
|
|
48
|
+
maxRetries: 3,
|
|
49
|
+
retryDelay: 1000,
|
|
50
|
+
onUnauthorized: async () => {
|
|
51
|
+
// Redirect to login page
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
window.location.href = '/login';
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
onRefreshTokenExpired: async () => {
|
|
57
|
+
console.error('Session expired. Please login again.');
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
window.location.href = '/login?expired=true';
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
onAuthenticated: async (config) => {
|
|
63
|
+
console.log('Authenticated', config.baseURL);
|
|
64
|
+
},
|
|
65
|
+
onRefreshToken: async () => {
|
|
66
|
+
return '';
|
|
67
|
+
},
|
|
68
|
+
...config,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.instance = axios.create({
|
|
72
|
+
baseURL: this.config.baseURL,
|
|
73
|
+
timeout: this.config.timeout,
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.setupInterceptors();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Setup request and response interceptors
|
|
84
|
+
*/
|
|
85
|
+
private setupInterceptors(): void {
|
|
86
|
+
// Request interceptor
|
|
87
|
+
this.instance.interceptors.request.use(
|
|
88
|
+
this.handleRequestFulfilled.bind(this),
|
|
89
|
+
this.handleRequestRejected.bind(this)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Response interceptor
|
|
93
|
+
this.instance.interceptors.response.use(
|
|
94
|
+
this.handleResponseFulfilled.bind(this),
|
|
95
|
+
this.handleResponseRejected.bind(this)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Attach JWT token to request headers
|
|
101
|
+
*/
|
|
102
|
+
private async handleRequestFulfilled(
|
|
103
|
+
config: InternalAxiosRequestConfig
|
|
104
|
+
): Promise<InternalAxiosRequestConfig> {
|
|
105
|
+
try {
|
|
106
|
+
if (this.config.onAuthenticated) {
|
|
107
|
+
this.config.onAuthenticated(config);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return config;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error attaching token to request:', error);
|
|
113
|
+
return config;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle request errors
|
|
119
|
+
*/
|
|
120
|
+
private handleRequestRejected(error: any): Promise<never> {
|
|
121
|
+
console.error('Request configuration error:', error);
|
|
122
|
+
return Promise.reject(error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Pass through successful responses
|
|
127
|
+
*/
|
|
128
|
+
private handleResponseFulfilled(response: AxiosResponse): AxiosResponse {
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle response errors with retry logic and token refresh
|
|
134
|
+
*/
|
|
135
|
+
private async handleResponseRejected(error: AxiosError): Promise<any> {
|
|
136
|
+
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
|
137
|
+
_retry?: boolean;
|
|
138
|
+
_retryCount?: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (!originalRequest) {
|
|
142
|
+
return Promise.reject(this.createApiError(error));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle 401 Unauthorized - attempt token refresh
|
|
146
|
+
if (error.response?.status === 401 && this.config.enableRefreshToken) {
|
|
147
|
+
return this.handleUnauthorizedError(error, originalRequest);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle network errors and 5xx errors with retry logic
|
|
151
|
+
if (this.shouldRetry(error, originalRequest)) {
|
|
152
|
+
return this.retryRequest(originalRequest);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create and reject with custom ApiError
|
|
156
|
+
return Promise.reject(this.createApiError(error));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle 401 errors with token refresh
|
|
161
|
+
*/
|
|
162
|
+
private async handleUnauthorizedError(
|
|
163
|
+
error: AxiosError,
|
|
164
|
+
originalRequest: InternalAxiosRequestConfig & { _retry?: boolean }
|
|
165
|
+
): Promise<any> {
|
|
166
|
+
// Prevent infinite loops
|
|
167
|
+
if (originalRequest._retry) {
|
|
168
|
+
if (this.config.onRefreshTokenExpired) {
|
|
169
|
+
await this.config.onRefreshTokenExpired();
|
|
170
|
+
}
|
|
171
|
+
return Promise.reject(this.createApiError(error));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
originalRequest._retry = true;
|
|
175
|
+
|
|
176
|
+
// If already refreshing, queue the request
|
|
177
|
+
if (this.isRefreshing) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
this.pendingRequests.push({ resolve, reject });
|
|
180
|
+
})
|
|
181
|
+
.then((token) => {
|
|
182
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
183
|
+
return this.instance(originalRequest);
|
|
184
|
+
})
|
|
185
|
+
.catch((err) => {
|
|
186
|
+
return Promise.reject(err);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.isRefreshing = true;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const newToken = await this.refreshToken();
|
|
194
|
+
|
|
195
|
+
// Update the original request with new token
|
|
196
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
197
|
+
|
|
198
|
+
// Resolve all pending requests with new token
|
|
199
|
+
this.processPendingRequests(null, newToken);
|
|
200
|
+
|
|
201
|
+
// Retry the original request
|
|
202
|
+
return this.instance(originalRequest);
|
|
203
|
+
} catch (refreshError) {
|
|
204
|
+
// Reject all pending requests
|
|
205
|
+
this.processPendingRequests(refreshError, null);
|
|
206
|
+
|
|
207
|
+
if (this.config.onUnauthorized) {
|
|
208
|
+
await this.config.onUnauthorized();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return Promise.reject(this.createApiError(error));
|
|
212
|
+
} finally {
|
|
213
|
+
this.isRefreshing = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Refresh the JWT token using the refresh token
|
|
219
|
+
*/
|
|
220
|
+
private async refreshToken(): Promise<string> {
|
|
221
|
+
try {
|
|
222
|
+
// const session = await auth();
|
|
223
|
+
//
|
|
224
|
+
// if (!session?.user || !('refreshToken' in session.user)) {
|
|
225
|
+
// throw new Error('No refresh token available');
|
|
226
|
+
// }
|
|
227
|
+
//
|
|
228
|
+
// const refreshToken = session.user.refreshToken as string;
|
|
229
|
+
const refreshToken = this.config.onRefreshToken();
|
|
230
|
+
|
|
231
|
+
// Call your refresh token endpoint
|
|
232
|
+
const response = await axios.post<{ jwtToken: string }>(
|
|
233
|
+
`${this.config.baseURL}/auth/refresh`,
|
|
234
|
+
{ refreshToken },
|
|
235
|
+
{
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const newToken = response.data.jwtToken;
|
|
243
|
+
|
|
244
|
+
// Update the session with the new token
|
|
245
|
+
// Note: You'll need to implement this based on your auth setup
|
|
246
|
+
// This might involve updating cookies or calling an API route
|
|
247
|
+
await this.updateSession(newToken);
|
|
248
|
+
|
|
249
|
+
return newToken;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error('Token refresh failed:', error);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Update session with new token
|
|
258
|
+
* Implement this based on your Next.js auth setup
|
|
259
|
+
*/
|
|
260
|
+
private async updateSession(newToken: string): Promise<void> {
|
|
261
|
+
// Example implementation - adjust based on your auth setup
|
|
262
|
+
try {
|
|
263
|
+
await fetch('/api/auth/update-token', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: {
|
|
266
|
+
'Content-Type': 'application/json',
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify({ jwtToken: newToken }),
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Failed to update session:', error);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Process all pending requests after token refresh
|
|
278
|
+
*/
|
|
279
|
+
private processPendingRequests(error: any, token: string | null): void {
|
|
280
|
+
this.pendingRequests.forEach((request) => {
|
|
281
|
+
if (error) {
|
|
282
|
+
request.reject(error);
|
|
283
|
+
} else if (token) {
|
|
284
|
+
request.resolve(token);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.pendingRequests = [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Determine if request should be retried
|
|
293
|
+
*/
|
|
294
|
+
private shouldRetry(
|
|
295
|
+
error: AxiosError,
|
|
296
|
+
config: InternalAxiosRequestConfig & { _retryCount?: number }
|
|
297
|
+
): boolean {
|
|
298
|
+
const retryCount = config._retryCount || 0;
|
|
299
|
+
|
|
300
|
+
// Don't retry if max retries exceeded
|
|
301
|
+
if (retryCount >= this.config.maxRetries) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Retry on network errors
|
|
306
|
+
if (!error.response) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Retry on 5xx server errors (except 501)
|
|
311
|
+
const status = error.response.status;
|
|
312
|
+
if (status >= 500 && status !== 501) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Retry on 429 (Too Many Requests)
|
|
317
|
+
if (status === 429) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Retry failed request with exponential backoff
|
|
326
|
+
*/
|
|
327
|
+
private async retryRequest(
|
|
328
|
+
config: InternalAxiosRequestConfig & { _retryCount?: number }
|
|
329
|
+
): Promise<any> {
|
|
330
|
+
config._retryCount = (config._retryCount || 0) + 1;
|
|
331
|
+
|
|
332
|
+
const delay = this.config.retryDelay * Math.pow(2, config._retryCount - 1);
|
|
333
|
+
|
|
334
|
+
await this.sleep(delay);
|
|
335
|
+
|
|
336
|
+
console.log(
|
|
337
|
+
`Retrying request (attempt ${config._retryCount}):`,
|
|
338
|
+
config.url
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
return this.instance(config);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Sleep helper for retry delays
|
|
346
|
+
*/
|
|
347
|
+
private sleep(ms: number): Promise<void> {
|
|
348
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create ApiError from AxiosError
|
|
353
|
+
*/
|
|
354
|
+
private createApiError(error: AxiosError): ApiError {
|
|
355
|
+
const status = error.response?.status || 0;
|
|
356
|
+
const problemDetail = this.extractProblemDetail(error);
|
|
357
|
+
|
|
358
|
+
return new ApiError(status, problemDetail, error);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Extract ProblemDetail from error response
|
|
363
|
+
*/
|
|
364
|
+
private extractProblemDetail(error: AxiosError): ProblemDetail | null {
|
|
365
|
+
if (!error.response?.data) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const data = error.response.data;
|
|
370
|
+
|
|
371
|
+
// Check if response matches ProblemDetail structure
|
|
372
|
+
if (
|
|
373
|
+
typeof data === 'object' &&
|
|
374
|
+
'type' in data &&
|
|
375
|
+
'title' in data &&
|
|
376
|
+
'status' in data
|
|
377
|
+
) {
|
|
378
|
+
return data as ProblemDetail;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* HTTP Methods with proper typing
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
389
|
+
const response = await this.instance.get<T>(url, config);
|
|
390
|
+
return response.data;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async post<T = any>(
|
|
394
|
+
url: string,
|
|
395
|
+
data?: any,
|
|
396
|
+
config?: AxiosRequestConfig
|
|
397
|
+
): Promise<T> {
|
|
398
|
+
const response = await this.instance.post<T>(url, data, config);
|
|
399
|
+
return response.data;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async put<T = any>(
|
|
403
|
+
url: string,
|
|
404
|
+
data?: any,
|
|
405
|
+
config?: AxiosRequestConfig
|
|
406
|
+
): Promise<T> {
|
|
407
|
+
const response = await this.instance.put<T>(url, data, config);
|
|
408
|
+
return response.data;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async patch<T = any>(
|
|
412
|
+
url: string,
|
|
413
|
+
data?: any,
|
|
414
|
+
config?: AxiosRequestConfig
|
|
415
|
+
): Promise<T> {
|
|
416
|
+
const response = await this.instance.patch<T>(url, data, config);
|
|
417
|
+
return response.data;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
421
|
+
const response = await this.instance.delete<T>(url, config);
|
|
422
|
+
return response.data;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get the underlying Axios instance for advanced usage
|
|
427
|
+
*/
|
|
428
|
+
getAxiosInstance(): AxiosInstance {
|
|
429
|
+
return this.instance;
|
|
430
|
+
}
|
|
431
|
+
}
|
package/src/lib/error.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { AxiosError, HttpStatusCode } from 'axios';
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spring Boot ProblemDetail structure
|
|
6
|
+
*/
|
|
7
|
+
export interface ProblemDetail {
|
|
8
|
+
type: string;
|
|
9
|
+
title: string;
|
|
10
|
+
status: HttpStatusCode;
|
|
11
|
+
detail?: string;
|
|
12
|
+
instance?: string;
|
|
13
|
+
errors?: Record<string, unknown>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom error class for API errors
|
|
19
|
+
*/
|
|
20
|
+
export class ApiError extends Error {
|
|
21
|
+
constructor(
|
|
22
|
+
public status: HttpStatusCode,
|
|
23
|
+
public problemDetail: ProblemDetail | null,
|
|
24
|
+
public originalError: Error,
|
|
25
|
+
message?: string
|
|
26
|
+
) {
|
|
27
|
+
super(
|
|
28
|
+
message ||
|
|
29
|
+
problemDetail?.detail ||
|
|
30
|
+
problemDetail?.title ||
|
|
31
|
+
originalError?.message ||
|
|
32
|
+
'An error occurred'
|
|
33
|
+
);
|
|
34
|
+
this.name = 'ApiError';
|
|
35
|
+
Object.setPrototypeOf(this, ApiError.prototype);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get isClientError(): boolean {
|
|
39
|
+
return this.status >= 400 && this.status < 500;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get isServerError(): boolean {
|
|
43
|
+
return this.status >= 500;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get isUnauthorized(): boolean {
|
|
47
|
+
return this.status === 401;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get isForbidden(): boolean {
|
|
51
|
+
return this.status === 403;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get isNotFound(): boolean {
|
|
55
|
+
return this.status === 404;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static builder<T>(): ApiErrorBuilder {
|
|
59
|
+
return new ApiErrorBuilder<T>();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static of(error: Error) {
|
|
63
|
+
if (error instanceof ApiError) {
|
|
64
|
+
return error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return ApiError.builder().originalError(error).build();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create ApiError from Zod validation error
|
|
72
|
+
*/
|
|
73
|
+
static fromZodError(zodError: ZodError) {
|
|
74
|
+
const errors: Record<string, string> = {};
|
|
75
|
+
|
|
76
|
+
zodError.errors.forEach((error) => {
|
|
77
|
+
error.path.forEach((path) => {
|
|
78
|
+
errors[path] = error.message;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return ApiError.builder()
|
|
83
|
+
.originalError(zodError)
|
|
84
|
+
.status(HttpStatusCode.BadRequest)
|
|
85
|
+
.message('Validation error')
|
|
86
|
+
.problemDetail('errors', errors)
|
|
87
|
+
.build();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class ApiErrorBuilder<
|
|
92
|
+
AdditionalProblemDetails = Record<string, unknown>
|
|
93
|
+
> {
|
|
94
|
+
private readonly _problemDetail: ProblemDetail;
|
|
95
|
+
private _status: HttpStatusCode;
|
|
96
|
+
private _originalError: Error;
|
|
97
|
+
private _message: string;
|
|
98
|
+
|
|
99
|
+
constructor() {
|
|
100
|
+
this._status = HttpStatusCode.InternalServerError;
|
|
101
|
+
this._problemDetail = {
|
|
102
|
+
status: this._status,
|
|
103
|
+
title: '',
|
|
104
|
+
type: 'about:blank',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set standard ProblemDetail fields
|
|
110
|
+
*/
|
|
111
|
+
problemDetail<K extends keyof (ProblemDetail & AdditionalProblemDetails)>(
|
|
112
|
+
key: K,
|
|
113
|
+
value: K extends keyof ProblemDetail
|
|
114
|
+
? ProblemDetail[K]
|
|
115
|
+
: K extends keyof AdditionalProblemDetails
|
|
116
|
+
? AdditionalProblemDetails[K]
|
|
117
|
+
: unknown
|
|
118
|
+
): ApiErrorBuilder<AdditionalProblemDetails> {
|
|
119
|
+
(this._problemDetail as any)[key] = value;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
originalError(error: Error): ApiErrorBuilder<AdditionalProblemDetails> {
|
|
124
|
+
this._originalError = error;
|
|
125
|
+
this._problemDetail.title = error.name;
|
|
126
|
+
|
|
127
|
+
if (error instanceof AxiosError) {
|
|
128
|
+
this.status(error.status);
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof ZodError) {
|
|
131
|
+
this.status(HttpStatusCode.BadRequest);
|
|
132
|
+
this.message('Validation error');
|
|
133
|
+
}
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
status(status: HttpStatusCode): ApiErrorBuilder<AdditionalProblemDetails> {
|
|
138
|
+
this._status = status;
|
|
139
|
+
this._problemDetail.status = status;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
message(msg: string): ApiErrorBuilder<AdditionalProblemDetails> {
|
|
144
|
+
this._message = msg;
|
|
145
|
+
this._problemDetail.detail = msg;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
build(): ApiError {
|
|
150
|
+
return new ApiError(
|
|
151
|
+
this._status,
|
|
152
|
+
this._problemDetail,
|
|
153
|
+
this._originalError,
|
|
154
|
+
this._message
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ProblemDetail } from '../error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* [api-response]
|
|
5
|
+
* next-feature@0.0.11-beta
|
|
6
|
+
* November 4th 2025, 6:37:27 pm
|
|
7
|
+
*/
|
|
8
|
+
export interface ApiResponse<Response> {
|
|
9
|
+
success?: boolean;
|
|
10
|
+
message?: string;
|
|
11
|
+
error?: ProblemDetail;
|
|
12
|
+
data: Response;
|
|
13
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ApiError } from '../error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract user-friendly error message from ApiError
|
|
5
|
+
*
|
|
6
|
+
* [get-error-message]
|
|
7
|
+
* next-feature@0.0.11-beta
|
|
8
|
+
* November 4th 2025, 11:47:45 am
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export function getErrorMessage(error: unknown): string {
|
|
12
|
+
if (error instanceof ApiError) {
|
|
13
|
+
// Use Spring Boot ProblemDetail information
|
|
14
|
+
if (error.problemDetail?.detail) {
|
|
15
|
+
return error.problemDetail.detail;
|
|
16
|
+
}
|
|
17
|
+
if (error.problemDetail?.title) {
|
|
18
|
+
return error.problemDetail.title;
|
|
19
|
+
}
|
|
20
|
+
return error.message;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (error instanceof Error) {
|
|
24
|
+
return error.message;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'An unexpected error occurred';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format ProblemDetail for display
|
|
32
|
+
*/
|
|
33
|
+
export function formatProblemDetail(error: ApiError): string {
|
|
34
|
+
if (!error.problemDetail) {
|
|
35
|
+
return error.message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parts: string[] = [];
|
|
39
|
+
|
|
40
|
+
if (error.problemDetail.title) {
|
|
41
|
+
parts.push(`Title: ${error.problemDetail.title}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error.problemDetail.detail) {
|
|
45
|
+
parts.push(`Detail: ${error.problemDetail.detail}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error.problemDetail.instance) {
|
|
49
|
+
parts.push(`Instance: ${error.problemDetail.instance}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return parts.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if error is a specific HTTP status
|
|
57
|
+
*/
|
|
58
|
+
export function isHttpStatus(error: unknown, status: number): boolean {
|
|
59
|
+
return error instanceof ApiError && error.status === status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle common API errors
|
|
64
|
+
*/
|
|
65
|
+
export function handleApiError(error: unknown): void {
|
|
66
|
+
if (!(error instanceof ApiError)) {
|
|
67
|
+
console.error('Unexpected error:', error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
switch (error.status) {
|
|
72
|
+
case 400:
|
|
73
|
+
console.error('Bad Request:', error.problemDetail?.detail);
|
|
74
|
+
break;
|
|
75
|
+
case 401:
|
|
76
|
+
console.error('Unauthorized - Please login');
|
|
77
|
+
break;
|
|
78
|
+
case 403:
|
|
79
|
+
console.error('Forbidden - Access denied');
|
|
80
|
+
break;
|
|
81
|
+
case 404:
|
|
82
|
+
console.error('Not Found:', error.problemDetail?.detail);
|
|
83
|
+
break;
|
|
84
|
+
case 409:
|
|
85
|
+
console.error('Conflict:', error.problemDetail?.detail);
|
|
86
|
+
break;
|
|
87
|
+
case 422:
|
|
88
|
+
console.error('Validation Error:', error.problemDetail);
|
|
89
|
+
break;
|
|
90
|
+
case 429:
|
|
91
|
+
console.error('Too Many Requests - Please slow down');
|
|
92
|
+
break;
|
|
93
|
+
case 500:
|
|
94
|
+
console.error('Server Error:', error.problemDetail?.detail);
|
|
95
|
+
break;
|
|
96
|
+
case 503:
|
|
97
|
+
console.error('Service Unavailable - Please try again later');
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
console.error(`Error ${error.status}:`, error.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validation error extractor for Spring Boot validation errors
|
|
106
|
+
*/
|
|
107
|
+
export function extractValidationErrors(
|
|
108
|
+
error: ApiError
|
|
109
|
+
): Record<string, string[]> | null {
|
|
110
|
+
if (!error.problemDetail || error.status !== 400) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Spring Boot often includes validation errors in a 'errors' or 'fieldErrors' property
|
|
115
|
+
const errors = error.problemDetail.errors || error.problemDetail.fieldErrors;
|
|
116
|
+
|
|
117
|
+
if (!errors) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Convert to a more usable format
|
|
122
|
+
if (Array.isArray(errors)) {
|
|
123
|
+
const result: Record<string, string[]> = {};
|
|
124
|
+
errors.forEach((err: any) => {
|
|
125
|
+
const field = err.field || err.property || 'general';
|
|
126
|
+
const message = err.message || err.defaultMessage || 'Validation error';
|
|
127
|
+
if (!result[field]) {
|
|
128
|
+
result[field] = [];
|
|
129
|
+
}
|
|
130
|
+
result[field].push(message);
|
|
131
|
+
});
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return errors as Record<string, string[]>;
|
|
136
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ProblemDetail } from '../error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* [show-error-toast]
|
|
5
|
+
* next-feature@0.0.11-beta
|
|
6
|
+
* November 4th 2025, 11:58:52 am
|
|
7
|
+
*/
|
|
8
|
+
export function showErrorToast(data: any) {
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getFieldError(
|
|
13
|
+
error: ProblemDetail | undefined,
|
|
14
|
+
fieldName: string
|
|
15
|
+
): string | undefined {
|
|
16
|
+
if (!error) return undefined;
|
|
17
|
+
|
|
18
|
+
const errors = error.errors as Record<string, string>;
|
|
19
|
+
return errors?.[fieldName];
|
|
20
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "react-jsx",
|
|
4
|
+
"allowJs": false,
|
|
5
|
+
"esModuleInterop": false,
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"types": ["vite/client"]
|
|
8
|
+
},
|
|
9
|
+
"files": [],
|
|
10
|
+
"include": [],
|
|
11
|
+
"references": [
|
|
12
|
+
{
|
|
13
|
+
"path": "./tsconfig.lib.json"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "./tsconfig.spec.json"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"extends": "../../tsconfig.base.json"
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"types": [
|
|
6
|
+
"node",
|
|
7
|
+
"@nx/react/typings/cssmodule.d.ts",
|
|
8
|
+
"@nx/react/typings/image.d.ts",
|
|
9
|
+
"vite/client",
|
|
10
|
+
"next",
|
|
11
|
+
"@nx/next/typings/image.d.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"exclude": [
|
|
15
|
+
"**/*.spec.ts",
|
|
16
|
+
"**/*.test.ts",
|
|
17
|
+
"**/*.spec.tsx",
|
|
18
|
+
"**/*.test.tsx",
|
|
19
|
+
"**/*.spec.js",
|
|
20
|
+
"**/*.test.js",
|
|
21
|
+
"**/*.spec.jsx",
|
|
22
|
+
"**/*.test.jsx",
|
|
23
|
+
"jest.config.ts",
|
|
24
|
+
"src/**/*.spec.ts",
|
|
25
|
+
"src/**/*.test.ts"
|
|
26
|
+
],
|
|
27
|
+
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"moduleResolution": "node10",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"types": ["jest", "node"]
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
"jest.config.ts",
|
|
12
|
+
"src/**/*.test.ts",
|
|
13
|
+
"src/**/*.spec.ts",
|
|
14
|
+
"src/**/*.test.tsx",
|
|
15
|
+
"src/**/*.spec.tsx",
|
|
16
|
+
"src/**/*.test.js",
|
|
17
|
+
"src/**/*.spec.js",
|
|
18
|
+
"src/**/*.test.jsx",
|
|
19
|
+
"src/**/*.spec.jsx",
|
|
20
|
+
"src/**/*.d.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/// <reference types='vitest' />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import dts from 'vite-plugin-dts';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
|
7
|
+
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
|
8
|
+
|
|
9
|
+
export default defineConfig(() => ({
|
|
10
|
+
root: __dirname,
|
|
11
|
+
cacheDir: '../../node_modules/.vite/clients/client',
|
|
12
|
+
plugins: [
|
|
13
|
+
react(),
|
|
14
|
+
nxViteTsPaths(),
|
|
15
|
+
nxCopyAssetsPlugin(['*.md']),
|
|
16
|
+
dts({
|
|
17
|
+
entryRoot: 'src',
|
|
18
|
+
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
// Uncomment this if you are using workers.
|
|
22
|
+
// worker: {
|
|
23
|
+
// plugins: [ nxViteTsPaths() ],
|
|
24
|
+
// },
|
|
25
|
+
// Configuration for building your library.
|
|
26
|
+
// See: https://vitejs.dev/guide/build.html#library-mode
|
|
27
|
+
build: {
|
|
28
|
+
outDir: '../../dist/clients/client',
|
|
29
|
+
emptyOutDir: true,
|
|
30
|
+
reportCompressedSize: true,
|
|
31
|
+
commonjsOptions: {
|
|
32
|
+
transformMixedEsModules: true,
|
|
33
|
+
},
|
|
34
|
+
lib: {
|
|
35
|
+
// Could also be a dictionary or array of multiple entry points.
|
|
36
|
+
entry: 'src/index.ts',
|
|
37
|
+
name: 'client',
|
|
38
|
+
fileName: 'index',
|
|
39
|
+
// Change this to the formats you want to support.
|
|
40
|
+
// Don't forget to update your package.json as well.
|
|
41
|
+
formats: ['es' as const],
|
|
42
|
+
},
|
|
43
|
+
rollupOptions: {
|
|
44
|
+
// External packages that should not be bundled into your library.
|
|
45
|
+
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}));
|