dalila 1.7.6 → 1.8.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/README.md +27 -0
- package/dist/http/adapter.d.ts +22 -0
- package/dist/http/adapter.js +264 -0
- package/dist/http/client.d.ts +48 -0
- package/dist/http/client.js +179 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +3 -0
- package/dist/http/types.d.ts +149 -0
- package/dist/http/types.js +20 -0
- package/dist/http/xsrf.d.ts +24 -0
- package/dist/http/xsrf.js +45 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -81,6 +81,10 @@ bind(document.getElementById('app')!, ctx);
|
|
|
81
81
|
- [Query](./docs/core/query.md) — Cached queries
|
|
82
82
|
- [Mutations](./docs/core/mutation.md) — Write operations
|
|
83
83
|
|
|
84
|
+
### HTTP
|
|
85
|
+
|
|
86
|
+
- [HTTP Client](./docs/http.md) — Native fetch-based client with XSRF protection and interceptors
|
|
87
|
+
|
|
84
88
|
### Forms
|
|
85
89
|
|
|
86
90
|
- [Forms](./docs/forms.md) — DOM-first form management with validation, field arrays, and accessibility
|
|
@@ -97,6 +101,7 @@ bind(document.getElementById('app')!, ctx);
|
|
|
97
101
|
dalila → signal, computed, effect, batch, ...
|
|
98
102
|
dalila/runtime → bind() for HTML templates
|
|
99
103
|
dalila/context → createContext, provide, inject
|
|
104
|
+
dalila/http → createHttpClient with XSRF protection
|
|
100
105
|
```
|
|
101
106
|
|
|
102
107
|
### Signals
|
|
@@ -166,6 +171,28 @@ theme.set('light'); // Saved automatically
|
|
|
166
171
|
// On reload: theme starts as 'light'
|
|
167
172
|
```
|
|
168
173
|
|
|
174
|
+
### HTTP Client
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { createHttpClient } from 'dalila/http';
|
|
178
|
+
|
|
179
|
+
const http = createHttpClient({
|
|
180
|
+
baseURL: 'https://api.example.com',
|
|
181
|
+
xsrf: true, // XSRF protection
|
|
182
|
+
onError: (error) => {
|
|
183
|
+
if (error.status === 401) window.location.href = '/login';
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// GET request
|
|
189
|
+
const response = await http.get('/users');
|
|
190
|
+
console.log(response.data);
|
|
191
|
+
|
|
192
|
+
// POST with auto JSON serialization
|
|
193
|
+
await http.post('/users', { name: 'John', email: 'john@example.com' });
|
|
194
|
+
```
|
|
195
|
+
|
|
169
196
|
### File-Based Routing
|
|
170
197
|
|
|
171
198
|
```txt
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch Adapter
|
|
3
|
+
*
|
|
4
|
+
* Native fetch-based HTTP adapter with timeout and abort support.
|
|
5
|
+
* Uses only browser/Node native APIs (fetch, AbortController, URL).
|
|
6
|
+
*/
|
|
7
|
+
import { type RequestConfig, type HttpResponse } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Execute an HTTP request using native fetch API.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Timeout support via AbortController
|
|
13
|
+
* - Manual cancellation via config.signal
|
|
14
|
+
* - Automatic JSON serialization
|
|
15
|
+
* - URL params handling
|
|
16
|
+
* - Response type parsing
|
|
17
|
+
*
|
|
18
|
+
* @param config - Request configuration
|
|
19
|
+
* @returns Promise that resolves to HttpResponse
|
|
20
|
+
* @throws HttpError on any failure
|
|
21
|
+
*/
|
|
22
|
+
export declare function fetchAdapter<T = any>(config: RequestConfig): Promise<HttpResponse<T>>;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch Adapter
|
|
3
|
+
*
|
|
4
|
+
* Native fetch-based HTTP adapter with timeout and abort support.
|
|
5
|
+
* Uses only browser/Node native APIs (fetch, AbortController, URL).
|
|
6
|
+
*/
|
|
7
|
+
import { HttpError } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Execute an HTTP request using native fetch API.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Timeout support via AbortController
|
|
13
|
+
* - Manual cancellation via config.signal
|
|
14
|
+
* - Automatic JSON serialization
|
|
15
|
+
* - URL params handling
|
|
16
|
+
* - Response type parsing
|
|
17
|
+
*
|
|
18
|
+
* @param config - Request configuration
|
|
19
|
+
* @returns Promise that resolves to HttpResponse
|
|
20
|
+
* @throws HttpError on any failure
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchAdapter(config) {
|
|
23
|
+
const { url = '', method = 'GET', headers = {}, data, params, timeout, signal: userSignal, responseType = 'json', baseURL = '', } = config;
|
|
24
|
+
// Build full URL
|
|
25
|
+
const fullUrl = buildUrl(baseURL, url, params);
|
|
26
|
+
// Setup abort handling (timeout + manual signal)
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const { signal, cleanup } = setupAbort(controller, timeout, userSignal);
|
|
29
|
+
// Build request headers
|
|
30
|
+
const requestHeaders = buildHeaders(headers, data);
|
|
31
|
+
// Build request body
|
|
32
|
+
const body = buildBody(data);
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
// Execute fetch
|
|
36
|
+
response = await fetch(fullUrl, {
|
|
37
|
+
method,
|
|
38
|
+
headers: requestHeaders,
|
|
39
|
+
body,
|
|
40
|
+
signal,
|
|
41
|
+
});
|
|
42
|
+
cleanup();
|
|
43
|
+
// Handle non-2xx responses
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const errorData = await parseResponseSafe(response, responseType);
|
|
46
|
+
throw new HttpError(`HTTP Error ${response.status}: ${response.statusText}`, 'http', config, {
|
|
47
|
+
status: response.status,
|
|
48
|
+
data: errorData,
|
|
49
|
+
response,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Check if response has a body
|
|
53
|
+
const hasBody = shouldParseResponseBody(response, method);
|
|
54
|
+
// Parse response (or return null for empty responses)
|
|
55
|
+
const responseData = hasBody
|
|
56
|
+
? await parseResponse(response, responseType)
|
|
57
|
+
: null;
|
|
58
|
+
return {
|
|
59
|
+
data: responseData,
|
|
60
|
+
status: response.status,
|
|
61
|
+
statusText: response.statusText,
|
|
62
|
+
headers: response.headers,
|
|
63
|
+
config,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
cleanup();
|
|
68
|
+
// Already an HttpError (from non-2xx response)
|
|
69
|
+
if (error instanceof HttpError) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
// AbortError (timeout or manual cancel)
|
|
73
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
74
|
+
const isTimeout = controller.signal.reason === 'timeout';
|
|
75
|
+
throw new HttpError(isTimeout ? `Request timeout after ${timeout}ms` : 'Request aborted', isTimeout ? 'timeout' : 'abort', config);
|
|
76
|
+
}
|
|
77
|
+
// Parse error (invalid JSON, malformed response body, etc)
|
|
78
|
+
if (error instanceof Error && error.message.startsWith('Failed to parse response')) {
|
|
79
|
+
throw new HttpError(error.message, 'parse', config, {
|
|
80
|
+
status: response?.status,
|
|
81
|
+
response: response,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Network error (DNS, connection refused, etc)
|
|
85
|
+
if (error instanceof TypeError) {
|
|
86
|
+
throw new HttpError(`Network error: ${error.message}`, 'network', config);
|
|
87
|
+
}
|
|
88
|
+
// Unknown error (treat as network error for backwards compatibility)
|
|
89
|
+
throw new HttpError(error instanceof Error ? error.message : String(error), 'network', config);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if a URL is absolute (starts with http://, https://, or //).
|
|
94
|
+
*/
|
|
95
|
+
function isAbsoluteUrl(url) {
|
|
96
|
+
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build full URL with baseURL and query params.
|
|
100
|
+
*/
|
|
101
|
+
function buildUrl(baseURL, url, params) {
|
|
102
|
+
// Only prepend baseURL if url is relative
|
|
103
|
+
let fullUrl;
|
|
104
|
+
if (baseURL && !isAbsoluteUrl(url)) {
|
|
105
|
+
fullUrl = `${baseURL}${url}`;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
fullUrl = url;
|
|
109
|
+
}
|
|
110
|
+
if (params && Object.keys(params).length > 0) {
|
|
111
|
+
// Build query string
|
|
112
|
+
const searchParams = new URLSearchParams();
|
|
113
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
114
|
+
searchParams.append(key, String(value));
|
|
115
|
+
});
|
|
116
|
+
const queryString = searchParams.toString();
|
|
117
|
+
// Append query string to original URL
|
|
118
|
+
// Preserve URL format (absolute http://, root-relative /, or path-relative)
|
|
119
|
+
if (fullUrl.includes('?')) {
|
|
120
|
+
// URL already has query params, append with &
|
|
121
|
+
fullUrl = `${fullUrl}&${queryString}`;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Add query params with ?
|
|
125
|
+
fullUrl = `${fullUrl}?${queryString}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return fullUrl;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build request headers with automatic Content-Type for JSON.
|
|
132
|
+
*/
|
|
133
|
+
function buildHeaders(headers, data) {
|
|
134
|
+
const result = { ...headers };
|
|
135
|
+
// Auto-add Content-Type for JSON data (but not for FormData/Blob/ArrayBuffer)
|
|
136
|
+
// Browser automatically sets correct Content-Type for FormData/Blob
|
|
137
|
+
if (data &&
|
|
138
|
+
typeof data === 'object' &&
|
|
139
|
+
!(data instanceof FormData) &&
|
|
140
|
+
!(data instanceof Blob) &&
|
|
141
|
+
!(data instanceof ArrayBuffer) &&
|
|
142
|
+
!result['Content-Type'] &&
|
|
143
|
+
!result['content-type']) {
|
|
144
|
+
result['Content-Type'] = 'application/json';
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build request body (auto-stringify JSON objects).
|
|
150
|
+
*/
|
|
151
|
+
function buildBody(data) {
|
|
152
|
+
// Only skip for null/undefined (not other falsy values like 0, false, "")
|
|
153
|
+
if (data === null || data === undefined) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
// Already serialized (string, FormData, Blob, etc)
|
|
157
|
+
if (typeof data === 'string' || data instanceof FormData || data instanceof Blob || data instanceof ArrayBuffer) {
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
// Serialize objects to JSON
|
|
161
|
+
if (typeof data === 'object') {
|
|
162
|
+
return JSON.stringify(data);
|
|
163
|
+
}
|
|
164
|
+
// Serialize primitives (numbers, booleans, etc) to string
|
|
165
|
+
return String(data);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Setup abort handling (timeout + manual signal).
|
|
169
|
+
*
|
|
170
|
+
* Returns:
|
|
171
|
+
* - signal: AbortSignal to pass to fetch
|
|
172
|
+
* - cleanup: Function to clear timeout
|
|
173
|
+
*/
|
|
174
|
+
function setupAbort(controller, timeout, userSignal) {
|
|
175
|
+
let timeoutId;
|
|
176
|
+
// Link user signal (manual cancellation)
|
|
177
|
+
if (userSignal) {
|
|
178
|
+
if (userSignal.aborted) {
|
|
179
|
+
controller.abort(userSignal.reason);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
userSignal.addEventListener('abort', () => {
|
|
183
|
+
controller.abort(userSignal.reason);
|
|
184
|
+
}, { once: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Setup timeout
|
|
188
|
+
if (timeout && timeout > 0) {
|
|
189
|
+
timeoutId = setTimeout(() => {
|
|
190
|
+
controller.abort('timeout');
|
|
191
|
+
}, timeout);
|
|
192
|
+
}
|
|
193
|
+
const cleanup = () => {
|
|
194
|
+
if (timeoutId !== undefined) {
|
|
195
|
+
clearTimeout(timeoutId);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
return { signal: controller.signal, cleanup };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if response should be parsed (has a body).
|
|
202
|
+
*
|
|
203
|
+
* Responses without body:
|
|
204
|
+
* - 204 No Content
|
|
205
|
+
* - 205 Reset Content
|
|
206
|
+
* - 304 Not Modified
|
|
207
|
+
* - HEAD requests
|
|
208
|
+
* - Content-Length: 0
|
|
209
|
+
*/
|
|
210
|
+
function shouldParseResponseBody(response, method) {
|
|
211
|
+
// Status codes that never have a body
|
|
212
|
+
if (response.status === 204 || response.status === 205 || response.status === 304) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
// HEAD requests never have a body
|
|
216
|
+
if (method.toUpperCase() === 'HEAD') {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
// Check Content-Length header
|
|
220
|
+
const contentLength = response.headers.get('Content-Length');
|
|
221
|
+
if (contentLength === '0') {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parse response based on responseType.
|
|
228
|
+
*/
|
|
229
|
+
async function parseResponse(response, responseType) {
|
|
230
|
+
try {
|
|
231
|
+
switch (responseType) {
|
|
232
|
+
case 'json':
|
|
233
|
+
return await response.json();
|
|
234
|
+
case 'text':
|
|
235
|
+
return await response.text();
|
|
236
|
+
case 'blob':
|
|
237
|
+
return await response.blob();
|
|
238
|
+
case 'arraybuffer':
|
|
239
|
+
return await response.arrayBuffer();
|
|
240
|
+
default:
|
|
241
|
+
return await response.json();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
throw new Error(`Failed to parse response as ${responseType}: ${error instanceof Error ? error.message : String(error)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Parse response safely for error handling (never throws).
|
|
250
|
+
*/
|
|
251
|
+
async function parseResponseSafe(response, responseType) {
|
|
252
|
+
try {
|
|
253
|
+
return await parseResponse(response, responseType);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// If parsing fails, try text as fallback
|
|
257
|
+
try {
|
|
258
|
+
return await response.text();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client
|
|
3
|
+
*
|
|
4
|
+
* Main HTTP client factory for Dalila framework.
|
|
5
|
+
* Provides a simple, Axios-inspired API with native fetch under the hood.
|
|
6
|
+
*/
|
|
7
|
+
import { type HttpClient, type HttpClientConfig } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create an HTTP client instance.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Global config (baseURL, headers, timeout)
|
|
13
|
+
* - Interceptors (onRequest, onResponse, onError)
|
|
14
|
+
* - Convenient methods (get, post, put, patch, delete)
|
|
15
|
+
* - Full TypeScript support
|
|
16
|
+
*
|
|
17
|
+
* Example:
|
|
18
|
+
* ```ts
|
|
19
|
+
* const http = createHttpClient({
|
|
20
|
+
* baseURL: 'https://api.example.com',
|
|
21
|
+
* headers: { 'Authorization': 'Bearer token' },
|
|
22
|
+
* timeout: 5000,
|
|
23
|
+
* onRequest: (config) => {
|
|
24
|
+
* console.log('Sending request:', config.url);
|
|
25
|
+
* return config;
|
|
26
|
+
* },
|
|
27
|
+
* onResponse: (response) => {
|
|
28
|
+
* console.log('Received response:', response.status);
|
|
29
|
+
* return response;
|
|
30
|
+
* },
|
|
31
|
+
* onError: (error) => {
|
|
32
|
+
* if (error.status === 401) {
|
|
33
|
+
* // Redirect to login
|
|
34
|
+
* window.location.href = '/login';
|
|
35
|
+
* }
|
|
36
|
+
* throw error;
|
|
37
|
+
* }
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Usage
|
|
41
|
+
* const response = await http.get('/users');
|
|
42
|
+
* await http.post('/login', { email, password });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @param config - Global client configuration
|
|
46
|
+
* @returns HTTP client instance
|
|
47
|
+
*/
|
|
48
|
+
export declare function createHttpClient(config?: HttpClientConfig): HttpClient;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client
|
|
3
|
+
*
|
|
4
|
+
* Main HTTP client factory for Dalila framework.
|
|
5
|
+
* Provides a simple, Axios-inspired API with native fetch under the hood.
|
|
6
|
+
*/
|
|
7
|
+
import { fetchAdapter } from './adapter.js';
|
|
8
|
+
import { getXsrfToken, requiresXsrfToken } from './xsrf.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create an HTTP client instance.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Global config (baseURL, headers, timeout)
|
|
14
|
+
* - Interceptors (onRequest, onResponse, onError)
|
|
15
|
+
* - Convenient methods (get, post, put, patch, delete)
|
|
16
|
+
* - Full TypeScript support
|
|
17
|
+
*
|
|
18
|
+
* Example:
|
|
19
|
+
* ```ts
|
|
20
|
+
* const http = createHttpClient({
|
|
21
|
+
* baseURL: 'https://api.example.com',
|
|
22
|
+
* headers: { 'Authorization': 'Bearer token' },
|
|
23
|
+
* timeout: 5000,
|
|
24
|
+
* onRequest: (config) => {
|
|
25
|
+
* console.log('Sending request:', config.url);
|
|
26
|
+
* return config;
|
|
27
|
+
* },
|
|
28
|
+
* onResponse: (response) => {
|
|
29
|
+
* console.log('Received response:', response.status);
|
|
30
|
+
* return response;
|
|
31
|
+
* },
|
|
32
|
+
* onError: (error) => {
|
|
33
|
+
* if (error.status === 401) {
|
|
34
|
+
* // Redirect to login
|
|
35
|
+
* window.location.href = '/login';
|
|
36
|
+
* }
|
|
37
|
+
* throw error;
|
|
38
|
+
* }
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Usage
|
|
42
|
+
* const response = await http.get('/users');
|
|
43
|
+
* await http.post('/login', { email, password });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @param config - Global client configuration
|
|
47
|
+
* @returns HTTP client instance
|
|
48
|
+
*/
|
|
49
|
+
export function createHttpClient(config = {}) {
|
|
50
|
+
const { baseURL = '', headers: defaultHeaders = {}, timeout: defaultTimeout, responseType: defaultResponseType = 'json', xsrf: xsrfConfig, onRequest, onResponse, onError, } = config;
|
|
51
|
+
// Parse XSRF config
|
|
52
|
+
const xsrf = xsrfConfig === true
|
|
53
|
+
? { cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN', safeMethods: ['GET', 'HEAD', 'OPTIONS'] }
|
|
54
|
+
: xsrfConfig === false || !xsrfConfig
|
|
55
|
+
? null
|
|
56
|
+
: { cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN', safeMethods: ['GET', 'HEAD', 'OPTIONS'], ...xsrfConfig };
|
|
57
|
+
/**
|
|
58
|
+
* Core request method.
|
|
59
|
+
* Merges global config with per-request config and executes interceptors.
|
|
60
|
+
*/
|
|
61
|
+
async function request(requestConfig) {
|
|
62
|
+
// Merge global config with request config
|
|
63
|
+
// Spread requestConfig first, then override with merged values
|
|
64
|
+
let mergedConfig = {
|
|
65
|
+
...requestConfig,
|
|
66
|
+
baseURL: requestConfig.baseURL ?? baseURL,
|
|
67
|
+
timeout: requestConfig.timeout ?? defaultTimeout,
|
|
68
|
+
responseType: requestConfig.responseType ?? defaultResponseType,
|
|
69
|
+
headers: mergeHeaders(defaultHeaders, requestConfig.headers),
|
|
70
|
+
};
|
|
71
|
+
// XSRF: Add token to header if configured
|
|
72
|
+
if (xsrf) {
|
|
73
|
+
const method = (mergedConfig.method || 'GET');
|
|
74
|
+
const safeMethods = xsrf.safeMethods || ['GET', 'HEAD', 'OPTIONS'];
|
|
75
|
+
if (requiresXsrfToken(method, safeMethods)) {
|
|
76
|
+
const cookieName = xsrf.cookieName || 'XSRF-TOKEN';
|
|
77
|
+
const token = getXsrfToken(cookieName);
|
|
78
|
+
if (token) {
|
|
79
|
+
const headerName = xsrf.headerName || 'X-XSRF-TOKEN';
|
|
80
|
+
mergedConfig.headers = {
|
|
81
|
+
...mergedConfig.headers,
|
|
82
|
+
[headerName]: token,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
// Run request interceptor
|
|
89
|
+
if (onRequest) {
|
|
90
|
+
mergedConfig = await onRequest(mergedConfig);
|
|
91
|
+
}
|
|
92
|
+
// Execute request
|
|
93
|
+
let response = await fetchAdapter(mergedConfig);
|
|
94
|
+
// Run response interceptor
|
|
95
|
+
if (onResponse) {
|
|
96
|
+
response = await onResponse(response);
|
|
97
|
+
}
|
|
98
|
+
return response;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Run error interceptor
|
|
102
|
+
if (onError) {
|
|
103
|
+
await onError(error);
|
|
104
|
+
}
|
|
105
|
+
// Rethrow error
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* GET request.
|
|
111
|
+
*/
|
|
112
|
+
function get(url, requestConfig) {
|
|
113
|
+
return request({
|
|
114
|
+
...requestConfig,
|
|
115
|
+
url,
|
|
116
|
+
method: 'GET',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* POST request.
|
|
121
|
+
*/
|
|
122
|
+
function post(url, data, requestConfig) {
|
|
123
|
+
return request({
|
|
124
|
+
...requestConfig,
|
|
125
|
+
url,
|
|
126
|
+
method: 'POST',
|
|
127
|
+
data,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* PUT request.
|
|
132
|
+
*/
|
|
133
|
+
function put(url, data, requestConfig) {
|
|
134
|
+
return request({
|
|
135
|
+
...requestConfig,
|
|
136
|
+
url,
|
|
137
|
+
method: 'PUT',
|
|
138
|
+
data,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* PATCH request.
|
|
143
|
+
*/
|
|
144
|
+
function patch(url, data, requestConfig) {
|
|
145
|
+
return request({
|
|
146
|
+
...requestConfig,
|
|
147
|
+
url,
|
|
148
|
+
method: 'PATCH',
|
|
149
|
+
data,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* DELETE request.
|
|
154
|
+
*/
|
|
155
|
+
function deleteFn(url, requestConfig) {
|
|
156
|
+
return request({
|
|
157
|
+
...requestConfig,
|
|
158
|
+
url,
|
|
159
|
+
method: 'DELETE',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
request,
|
|
164
|
+
get,
|
|
165
|
+
post,
|
|
166
|
+
put,
|
|
167
|
+
patch,
|
|
168
|
+
delete: deleteFn,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Merge headers (per-request headers override global headers).
|
|
173
|
+
*/
|
|
174
|
+
function mergeHeaders(globalHeaders, requestHeaders) {
|
|
175
|
+
return {
|
|
176
|
+
...globalHeaders,
|
|
177
|
+
...requestHeaders,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createHttpClient } from './client.js';
|
|
2
|
+
export { fetchAdapter } from './adapter.js';
|
|
3
|
+
export type { HttpClient, HttpClientConfig, HttpMethod, HttpResponse, HttpErrorType, RequestConfig, RequestInterceptor, ResponseInterceptor, ErrorInterceptor, Interceptors, XsrfConfig, } from './types.js';
|
|
4
|
+
export { HttpError } from './types.js';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the Dalila HTTP client.
|
|
5
|
+
* Designed for simplicity and SPA-first workflows.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* HTTP methods supported by the client.
|
|
9
|
+
*/
|
|
10
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
11
|
+
/**
|
|
12
|
+
* Request configuration.
|
|
13
|
+
*/
|
|
14
|
+
export interface RequestConfig {
|
|
15
|
+
/** Request URL (relative to baseURL if configured). */
|
|
16
|
+
url?: string;
|
|
17
|
+
/** HTTP method (defaults to GET). */
|
|
18
|
+
method?: HttpMethod;
|
|
19
|
+
/** Request headers. */
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
/** Request body (auto-serialized to JSON if object). */
|
|
22
|
+
data?: any;
|
|
23
|
+
/** URL query parameters. */
|
|
24
|
+
params?: Record<string, string | number | boolean>;
|
|
25
|
+
/** Request timeout in milliseconds. */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** AbortSignal for manual cancellation. */
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
/** Response type (defaults to 'json'). */
|
|
30
|
+
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
|
|
31
|
+
/** Base URL for this request (overrides global baseURL). */
|
|
32
|
+
baseURL?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* HTTP response.
|
|
36
|
+
*/
|
|
37
|
+
export interface HttpResponse<T = any> {
|
|
38
|
+
/** Response data (parsed). */
|
|
39
|
+
data: T;
|
|
40
|
+
/** HTTP status code. */
|
|
41
|
+
status: number;
|
|
42
|
+
/** HTTP status text. */
|
|
43
|
+
statusText: string;
|
|
44
|
+
/** Response headers. */
|
|
45
|
+
headers: Headers;
|
|
46
|
+
/** Original request config. */
|
|
47
|
+
config: RequestConfig;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Error types for predictable error handling.
|
|
51
|
+
*/
|
|
52
|
+
export type HttpErrorType = 'network' | 'timeout' | 'abort' | 'http' | 'parse';
|
|
53
|
+
/**
|
|
54
|
+
* HTTP error with structured information.
|
|
55
|
+
*/
|
|
56
|
+
export declare class HttpError extends Error {
|
|
57
|
+
/** Error type (network, timeout, http, etc). */
|
|
58
|
+
type: HttpErrorType;
|
|
59
|
+
/** HTTP status code (if available). */
|
|
60
|
+
status?: number;
|
|
61
|
+
/** Response data (if available). */
|
|
62
|
+
data?: any;
|
|
63
|
+
/** Original request config. */
|
|
64
|
+
config: RequestConfig;
|
|
65
|
+
/** Native Response object (if available). */
|
|
66
|
+
response?: Response;
|
|
67
|
+
constructor(message: string, type: HttpErrorType, config: RequestConfig, options?: {
|
|
68
|
+
status?: number;
|
|
69
|
+
data?: any;
|
|
70
|
+
response?: Response;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Request interceptor.
|
|
75
|
+
* Called before each request is sent.
|
|
76
|
+
* Can modify config or throw to abort the request.
|
|
77
|
+
*/
|
|
78
|
+
export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
|
|
79
|
+
/**
|
|
80
|
+
* Response interceptor.
|
|
81
|
+
* Called after each successful response.
|
|
82
|
+
* Can transform the response or throw to convert success to error.
|
|
83
|
+
*/
|
|
84
|
+
export type ResponseInterceptor = <T = any>(response: HttpResponse<T>) => HttpResponse<T> | Promise<HttpResponse<T>>;
|
|
85
|
+
/**
|
|
86
|
+
* Error interceptor.
|
|
87
|
+
* Called when a request fails.
|
|
88
|
+
* Can recover from errors or rethrow.
|
|
89
|
+
*/
|
|
90
|
+
export type ErrorInterceptor = (error: HttpError) => never | Promise<never>;
|
|
91
|
+
/**
|
|
92
|
+
* Interceptor hooks.
|
|
93
|
+
*/
|
|
94
|
+
export interface Interceptors {
|
|
95
|
+
/** Called before each request. */
|
|
96
|
+
onRequest?: RequestInterceptor;
|
|
97
|
+
/** Called after each successful response. */
|
|
98
|
+
onResponse?: ResponseInterceptor;
|
|
99
|
+
/** Called when a request fails. */
|
|
100
|
+
onError?: ErrorInterceptor;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* XSRF (CSRF) protection configuration.
|
|
104
|
+
*/
|
|
105
|
+
export interface XsrfConfig {
|
|
106
|
+
/** Name of the cookie where the token is stored (default: 'XSRF-TOKEN'). */
|
|
107
|
+
cookieName?: string;
|
|
108
|
+
/** Name of the header to send the token in (default: 'X-XSRF-TOKEN'). */
|
|
109
|
+
headerName?: string;
|
|
110
|
+
/** HTTP methods that don't require XSRF protection (default: ['GET', 'HEAD', 'OPTIONS']). */
|
|
111
|
+
safeMethods?: HttpMethod[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* HTTP client configuration.
|
|
115
|
+
*/
|
|
116
|
+
export interface HttpClientConfig extends Interceptors {
|
|
117
|
+
/** Base URL for all requests. */
|
|
118
|
+
baseURL?: string;
|
|
119
|
+
/** Default headers for all requests. */
|
|
120
|
+
headers?: Record<string, string>;
|
|
121
|
+
/** Default timeout in milliseconds. */
|
|
122
|
+
timeout?: number;
|
|
123
|
+
/** Default response type. */
|
|
124
|
+
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
|
|
125
|
+
/**
|
|
126
|
+
* XSRF (CSRF) protection configuration.
|
|
127
|
+
* - `true`: Enable with defaults (cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN')
|
|
128
|
+
* - `false`: Disable XSRF protection
|
|
129
|
+
* - `XsrfConfig`: Custom configuration
|
|
130
|
+
*/
|
|
131
|
+
xsrf?: boolean | XsrfConfig;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* HTTP client instance.
|
|
135
|
+
*/
|
|
136
|
+
export interface HttpClient {
|
|
137
|
+
/** Make a request with full config. */
|
|
138
|
+
request<T = any>(config: RequestConfig): Promise<HttpResponse<T>>;
|
|
139
|
+
/** GET request. */
|
|
140
|
+
get<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>): Promise<HttpResponse<T>>;
|
|
141
|
+
/** POST request. */
|
|
142
|
+
post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<HttpResponse<T>>;
|
|
143
|
+
/** PUT request. */
|
|
144
|
+
put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<HttpResponse<T>>;
|
|
145
|
+
/** PATCH request. */
|
|
146
|
+
patch<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<HttpResponse<T>>;
|
|
147
|
+
/** DELETE request. */
|
|
148
|
+
delete<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>): Promise<HttpResponse<T>>;
|
|
149
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the Dalila HTTP client.
|
|
5
|
+
* Designed for simplicity and SPA-first workflows.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* HTTP error with structured information.
|
|
9
|
+
*/
|
|
10
|
+
export class HttpError extends Error {
|
|
11
|
+
constructor(message, type, config, options) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'HttpError';
|
|
14
|
+
this.type = type;
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.status = options?.status;
|
|
17
|
+
this.data = options?.data;
|
|
18
|
+
this.response = options?.response;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSRF (CSRF) Protection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers for reading XSRF tokens from cookies/meta tags and
|
|
5
|
+
* determining which HTTP methods require protection.
|
|
6
|
+
*/
|
|
7
|
+
import type { HttpMethod } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Extract value from a cookie by name.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getCookie(name: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Extract XSRF token from meta tag.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getMetaTag(name: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Get XSRF token (tries cookie first, then meta tag fallback).
|
|
18
|
+
*/
|
|
19
|
+
export declare function getXsrfToken(cookieName: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Check if HTTP method requires XSRF token.
|
|
22
|
+
* Safe methods (GET, HEAD, OPTIONS) don't need tokens.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requiresXsrfToken(method: HttpMethod, safeMethods: HttpMethod[]): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSRF (CSRF) Protection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers for reading XSRF tokens from cookies/meta tags and
|
|
5
|
+
* determining which HTTP methods require protection.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extract value from a cookie by name.
|
|
9
|
+
*/
|
|
10
|
+
export function getCookie(name) {
|
|
11
|
+
if (typeof document === 'undefined')
|
|
12
|
+
return null;
|
|
13
|
+
const value = `; ${document.cookie}`;
|
|
14
|
+
const parts = value.split(`; ${name}=`);
|
|
15
|
+
if (parts.length === 2) {
|
|
16
|
+
return parts.pop()?.split(';').shift() || null;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract XSRF token from meta tag.
|
|
22
|
+
*/
|
|
23
|
+
export function getMetaTag(name) {
|
|
24
|
+
if (typeof document === 'undefined')
|
|
25
|
+
return null;
|
|
26
|
+
const element = document.querySelector(`meta[name="${name}"]`);
|
|
27
|
+
return element?.getAttribute('content') || null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get XSRF token (tries cookie first, then meta tag fallback).
|
|
31
|
+
*/
|
|
32
|
+
export function getXsrfToken(cookieName) {
|
|
33
|
+
const fromCookie = getCookie(cookieName);
|
|
34
|
+
if (fromCookie)
|
|
35
|
+
return fromCookie;
|
|
36
|
+
// Fallback to common meta tag names
|
|
37
|
+
return getMetaTag('csrf-token') || getMetaTag('xsrf-token');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if HTTP method requires XSRF token.
|
|
41
|
+
* Safe methods (GET, HEAD, OPTIONS) don't need tokens.
|
|
42
|
+
*/
|
|
43
|
+
export function requiresXsrfToken(method, safeMethods) {
|
|
44
|
+
return !safeMethods.includes(method);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dalila",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "DOM-first reactive framework based on signals",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"types": "./dist/form/index.d.ts",
|
|
39
39
|
"default": "./dist/form/index.js"
|
|
40
40
|
},
|
|
41
|
+
"./http": {
|
|
42
|
+
"types": "./dist/http/index.d.ts",
|
|
43
|
+
"default": "./dist/http/index.js"
|
|
44
|
+
},
|
|
41
45
|
"./components/ui": {
|
|
42
46
|
"types": "./dist/components/ui/index.d.ts",
|
|
43
47
|
"default": "./dist/components/ui/index.js"
|