juxscript 1.0.59 → 1.0.61
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 +32 -1
- package/bin/cli.js +13 -33
- package/docs/grid.png +0 -0
- package/juxconfig.example.js +24 -2
- package/lib/components/base/BaseComponent.ts +20 -0
- package/lib/components/chart.ts +231 -0
- package/lib/components/container.ts +76 -118
- package/lib/components/grid.ts +291 -0
- package/lib/components/input.ts +55 -1
- package/lib/jux.ts +10 -29
- package/lib/utils/fetch.ts +553 -0
- package/machinery/ast.js +347 -0
- package/machinery/bundleAssets.js +0 -0
- package/machinery/bundleJux.js +0 -0
- package/machinery/bundleVendors.js +0 -0
- package/machinery/compiler.js +427 -151
- package/machinery/server.js +16 -2
- package/machinery/ts-shim.js +46 -0
- package/package.json +5 -1
- package/presets/default/all.jux +2 -2
- package/presets/default/layout.css +120 -0
- package/lib/components/charts/areachart.ts +0 -315
- package/lib/components/charts/barchart.ts +0 -421
- package/lib/components/charts/doughnutchart.ts +0 -263
- package/lib/components/charts/lib/BaseChart.ts +0 -389
- package/lib/components/charts/lib/chart-types.ts +0 -159
- package/lib/components/charts/lib/chart-utils.ts +0 -160
- package/lib/components/charts/lib/chart.ts +0 -707
- package/lib/components/charts/lib/charts.js +0 -126
- package/lib/components/docs-data.json +0 -2075
- package/lib/components/kpicard.ts +0 -640
package/lib/jux.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { state } from './reactivity/state.js';
|
|
7
|
+
import { fetchAPI, type FetchConfig, type FetchResult } from './utils/fetch.js';
|
|
7
8
|
import { Guard, type GuardOptions, guard } from './components/guard.js';
|
|
8
9
|
import { Data, data } from './components/data.js';
|
|
9
10
|
import { table, Table, type TableOptions } from './components/table.js';
|
|
@@ -54,14 +55,8 @@ import { divider, Divider, type DividerOptions } from './components/divider.js';
|
|
|
54
55
|
import { icon as iconComponent, Icon, type IconOptions } from './components/icon.js';
|
|
55
56
|
import { renderIcon, renderEmoji } from './components/icons.js';
|
|
56
57
|
import { sidebar, Sidebar, type SidebarOptions } from './components/sidebar.js';
|
|
58
|
+
import { grid, Grid } from './components/grid.js';
|
|
57
59
|
|
|
58
|
-
// ✅ NEW: Chart imports from /charts folder
|
|
59
|
-
import { chart, Chart, type ChartOptions } from './components/charts/lib/chart.js';
|
|
60
|
-
import { barchart, BarChart, type BarChartOptions } from './components/charts/barchart.js';
|
|
61
|
-
import { areachart, AreaChart, type AreaChartOptions } from './components/charts/areachart.js';
|
|
62
|
-
import { doughnutchart, DoughnutChart, type DoughnutChartOptions } from './components/charts/doughnutchart.js';
|
|
63
|
-
import { kpicard, KPICard, type KPICardOptions } from './components/kpicard.js';
|
|
64
|
-
import type { ChartDataPoint } from './components/charts/lib/BaseChart.js';
|
|
65
60
|
|
|
66
61
|
/* -------------------------
|
|
67
62
|
* Type Exports
|
|
@@ -83,7 +78,6 @@ export type {
|
|
|
83
78
|
NavItem,
|
|
84
79
|
ViewOptions,
|
|
85
80
|
TableOptions,
|
|
86
|
-
ChartOptions,
|
|
87
81
|
CodeOptions,
|
|
88
82
|
InputOptions,
|
|
89
83
|
CardOptions,
|
|
@@ -112,11 +106,6 @@ export type {
|
|
|
112
106
|
RequestInfo,
|
|
113
107
|
HeadingOptions,
|
|
114
108
|
ParagraphOptions,
|
|
115
|
-
BarChartOptions,
|
|
116
|
-
AreaChartOptions,
|
|
117
|
-
DoughnutChartOptions,
|
|
118
|
-
KPICardOptions,
|
|
119
|
-
ChartDataPoint,
|
|
120
109
|
DividerOptions,
|
|
121
110
|
IconOptions,
|
|
122
111
|
SidebarOptions
|
|
@@ -139,7 +128,6 @@ export {
|
|
|
139
128
|
Tabs,
|
|
140
129
|
Menu,
|
|
141
130
|
Nav,
|
|
142
|
-
Chart,
|
|
143
131
|
Code,
|
|
144
132
|
Input,
|
|
145
133
|
View,
|
|
@@ -167,13 +155,10 @@ export {
|
|
|
167
155
|
Req,
|
|
168
156
|
Heading,
|
|
169
157
|
Paragraph,
|
|
170
|
-
BarChart,
|
|
171
|
-
AreaChart,
|
|
172
|
-
DoughnutChart,
|
|
173
|
-
KPICard,
|
|
174
158
|
Divider,
|
|
175
159
|
Icon,
|
|
176
|
-
Sidebar
|
|
160
|
+
Sidebar,
|
|
161
|
+
Grid
|
|
177
162
|
};
|
|
178
163
|
|
|
179
164
|
/* -------------------------
|
|
@@ -185,6 +170,7 @@ export interface JuxAPI {
|
|
|
185
170
|
apiUrl: string;
|
|
186
171
|
param(name: string): string | null;
|
|
187
172
|
req: typeof req;
|
|
173
|
+
fetch: typeof fetchAPI;
|
|
188
174
|
|
|
189
175
|
// Icon utilities
|
|
190
176
|
icon: typeof renderIcon;
|
|
@@ -213,7 +199,6 @@ export interface JuxAPI {
|
|
|
213
199
|
nav: typeof nav;
|
|
214
200
|
div: typeof div;
|
|
215
201
|
span: typeof span;
|
|
216
|
-
chart: typeof chart;
|
|
217
202
|
code: typeof code;
|
|
218
203
|
|
|
219
204
|
view: typeof view;
|
|
@@ -240,10 +225,6 @@ export interface JuxAPI {
|
|
|
240
225
|
fileupload: typeof fileupload;
|
|
241
226
|
heading: typeof heading;
|
|
242
227
|
paragraph: typeof paragraph;
|
|
243
|
-
barchart: typeof barchart;
|
|
244
|
-
areachart: typeof areachart;
|
|
245
|
-
doughnutchart: typeof doughnutchart;
|
|
246
|
-
kpicard: typeof kpicard;
|
|
247
228
|
divider: typeof divider;
|
|
248
229
|
|
|
249
230
|
// Input factories
|
|
@@ -259,6 +240,7 @@ export interface JuxAPI {
|
|
|
259
240
|
date: typeof date;
|
|
260
241
|
time: typeof time;
|
|
261
242
|
color: typeof color;
|
|
243
|
+
grid: typeof grid;
|
|
262
244
|
}
|
|
263
245
|
|
|
264
246
|
/* -------------------------
|
|
@@ -287,6 +269,9 @@ class Jux implements JuxAPI {
|
|
|
287
269
|
// Request utilities
|
|
288
270
|
req = req;
|
|
289
271
|
|
|
272
|
+
// Fetch utility
|
|
273
|
+
fetch = fetchAPI;
|
|
274
|
+
|
|
290
275
|
// Icon utilities
|
|
291
276
|
icon = renderIcon;
|
|
292
277
|
emoji = renderEmoji;
|
|
@@ -315,7 +300,6 @@ class Jux implements JuxAPI {
|
|
|
315
300
|
nav = nav;
|
|
316
301
|
div = div;
|
|
317
302
|
span = span;
|
|
318
|
-
chart = chart;
|
|
319
303
|
view = view;
|
|
320
304
|
app = app;
|
|
321
305
|
include = include;
|
|
@@ -340,10 +324,6 @@ class Jux implements JuxAPI {
|
|
|
340
324
|
fileupload = fileupload;
|
|
341
325
|
heading = heading;
|
|
342
326
|
paragraph = paragraph;
|
|
343
|
-
barchart = barchart;
|
|
344
|
-
areachart = areachart;
|
|
345
|
-
doughnutchart = doughnutchart;
|
|
346
|
-
kpicard = kpicard;
|
|
347
327
|
divider = divider;
|
|
348
328
|
|
|
349
329
|
// Input factories
|
|
@@ -359,6 +339,7 @@ class Jux implements JuxAPI {
|
|
|
359
339
|
date = date;
|
|
360
340
|
time = time;
|
|
361
341
|
color = color;
|
|
342
|
+
grid = grid;
|
|
362
343
|
}
|
|
363
344
|
/**
|
|
364
345
|
* Global jux singleton instance
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jux Fetch Utility
|
|
3
|
+
*
|
|
4
|
+
* A lightweight fetch wrapper with sensible defaults, error handling, and method chaining.
|
|
5
|
+
* Includes configuration helpers for juxconfig.js integration.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* // Configure once (optional)
|
|
9
|
+
* jux.fetch.config({
|
|
10
|
+
* baseUrl: 'https://api.example.com',
|
|
11
|
+
* credentials: 'include',
|
|
12
|
+
* timeout: 10000,
|
|
13
|
+
* log: 'errors',
|
|
14
|
+
* onUnauthorized: () => window.location.href = '/login'
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Simple GET
|
|
18
|
+
* const { data, error } = await jux.fetch('/users').send();
|
|
19
|
+
*
|
|
20
|
+
* // Method chaining
|
|
21
|
+
* const { data, error } = await jux.fetch('/users')
|
|
22
|
+
* .method('POST')
|
|
23
|
+
* .body({ name: 'John' })
|
|
24
|
+
* .params({ limit: 10 })
|
|
25
|
+
* .timeout(5000)
|
|
26
|
+
* .log(true)
|
|
27
|
+
* .send();
|
|
28
|
+
*
|
|
29
|
+
* // With juxconfig services
|
|
30
|
+
* const { data } = await jux.fetch('/users')
|
|
31
|
+
* .baseUrl(jux.fetch.getServiceUrl('database'))
|
|
32
|
+
* .send();
|
|
33
|
+
*
|
|
34
|
+
* // Service client helper
|
|
35
|
+
* const db = jux.fetch.serviceClient('database');
|
|
36
|
+
* const { data } = await db.fetch('/users').send();
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type LogLevel = boolean | 'errors';
|
|
40
|
+
|
|
41
|
+
export interface FetchConfig {
|
|
42
|
+
baseUrl?: string;
|
|
43
|
+
credentials?: RequestCredentials;
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
timeout?: number;
|
|
46
|
+
log?: LogLevel;
|
|
47
|
+
onUnauthorized?: () => void;
|
|
48
|
+
onError?: (error: FetchError) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FetchOptions extends Omit<RequestInit, 'body'> {
|
|
52
|
+
params?: Record<string, any>;
|
|
53
|
+
body?: any;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
log?: LogLevel;
|
|
56
|
+
onUnauthorized?: () => void;
|
|
57
|
+
onError?: (error: FetchError) => void;
|
|
58
|
+
parseResponse?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FetchError {
|
|
62
|
+
message: string;
|
|
63
|
+
status?: number;
|
|
64
|
+
statusText?: string;
|
|
65
|
+
data?: any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface FetchResult<T = any> {
|
|
69
|
+
data: T | null;
|
|
70
|
+
error: FetchError | null;
|
|
71
|
+
status: number;
|
|
72
|
+
response: Response;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Global configuration
|
|
76
|
+
let globalConfig: FetchConfig = {
|
|
77
|
+
credentials: 'same-origin',
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json'
|
|
80
|
+
},
|
|
81
|
+
timeout: 30000,
|
|
82
|
+
log: false
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Configure global fetch defaults
|
|
87
|
+
*/
|
|
88
|
+
export function configureFetch(config: FetchConfig): void {
|
|
89
|
+
globalConfig = {
|
|
90
|
+
...globalConfig,
|
|
91
|
+
...config,
|
|
92
|
+
headers: {
|
|
93
|
+
...globalConfig.headers,
|
|
94
|
+
...config.headers
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build URL with query parameters
|
|
101
|
+
*/
|
|
102
|
+
function buildUrl(url: string, params?: Record<string, any>): string {
|
|
103
|
+
if (!params || Object.keys(params).length === 0) {
|
|
104
|
+
return url;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const searchParams = new URLSearchParams();
|
|
108
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
109
|
+
if (value !== undefined && value !== null) {
|
|
110
|
+
searchParams.append(key, String(value));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const queryString = searchParams.toString();
|
|
115
|
+
if (!queryString) return url;
|
|
116
|
+
|
|
117
|
+
return url.includes('?')
|
|
118
|
+
? `${url}&${queryString}`
|
|
119
|
+
: `${url}?${queryString}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Main fetch function
|
|
124
|
+
*/
|
|
125
|
+
async function executeFetch<T = any>(
|
|
126
|
+
url: string,
|
|
127
|
+
options: FetchOptions = {}
|
|
128
|
+
): Promise<FetchResult<T>> {
|
|
129
|
+
const {
|
|
130
|
+
params,
|
|
131
|
+
body,
|
|
132
|
+
timeout = globalConfig.timeout,
|
|
133
|
+
log = globalConfig.log,
|
|
134
|
+
onUnauthorized = globalConfig.onUnauthorized,
|
|
135
|
+
onError = globalConfig.onError,
|
|
136
|
+
parseResponse = true,
|
|
137
|
+
headers = {},
|
|
138
|
+
...fetchOptions
|
|
139
|
+
} = options;
|
|
140
|
+
|
|
141
|
+
// Build full URL
|
|
142
|
+
let fullUrl = url;
|
|
143
|
+
|
|
144
|
+
// Add base URL if configured and URL is relative
|
|
145
|
+
if (globalConfig.baseUrl && !url.startsWith('http://') && !url.startsWith('https://')) {
|
|
146
|
+
fullUrl = `${globalConfig.baseUrl}${url.startsWith('/') ? url : `/${url}`}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add query params
|
|
150
|
+
fullUrl = buildUrl(fullUrl, params);
|
|
151
|
+
|
|
152
|
+
// Merge headers
|
|
153
|
+
const mergedHeaders = {
|
|
154
|
+
...globalConfig.headers,
|
|
155
|
+
...headers
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Prepare request init
|
|
159
|
+
const requestInit: RequestInit = {
|
|
160
|
+
...fetchOptions,
|
|
161
|
+
headers: mergedHeaders,
|
|
162
|
+
credentials: options.credentials ?? globalConfig.credentials
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Stringify body if it's an object
|
|
166
|
+
if (body !== undefined) {
|
|
167
|
+
if (body instanceof FormData) {
|
|
168
|
+
requestInit.body = body;
|
|
169
|
+
// Remove Content-Type header for FormData (browser sets it with boundary)
|
|
170
|
+
delete (requestInit.headers as Record<string, string>)['Content-Type'];
|
|
171
|
+
} else if (typeof body === 'object') {
|
|
172
|
+
requestInit.body = JSON.stringify(body);
|
|
173
|
+
} else {
|
|
174
|
+
requestInit.body = body;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Log request
|
|
179
|
+
if (log === true) {
|
|
180
|
+
console.log(`[JUX Fetch] ${requestInit.method || 'GET'} ${fullUrl}`, {
|
|
181
|
+
headers: requestInit.headers,
|
|
182
|
+
body: requestInit.body,
|
|
183
|
+
params
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Setup timeout
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
|
|
190
|
+
|
|
191
|
+
if (!requestInit.signal) {
|
|
192
|
+
requestInit.signal = controller.signal;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(fullUrl, requestInit);
|
|
197
|
+
|
|
198
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
199
|
+
|
|
200
|
+
// Handle unauthorized
|
|
201
|
+
if (response.status === 401 || response.status === 403) {
|
|
202
|
+
if (onUnauthorized) {
|
|
203
|
+
onUnauthorized();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Parse response
|
|
208
|
+
let data: T | null = null;
|
|
209
|
+
let errorData: any = null;
|
|
210
|
+
|
|
211
|
+
if (parseResponse) {
|
|
212
|
+
const contentType = response.headers.get('content-type');
|
|
213
|
+
|
|
214
|
+
if (contentType?.includes('application/json')) {
|
|
215
|
+
try {
|
|
216
|
+
const json = await response.json();
|
|
217
|
+
if (response.ok) {
|
|
218
|
+
data = json;
|
|
219
|
+
} else {
|
|
220
|
+
errorData = json;
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
// JSON parse error
|
|
224
|
+
}
|
|
225
|
+
} else if (response.ok) {
|
|
226
|
+
// Non-JSON response, try to get text
|
|
227
|
+
try {
|
|
228
|
+
data = (await response.text()) as any;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// Ignore text parse errors
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if request was successful
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
const error: FetchError = {
|
|
238
|
+
message: errorData?.message || errorData?.error || response.statusText || 'Request failed',
|
|
239
|
+
status: response.status,
|
|
240
|
+
statusText: response.statusText,
|
|
241
|
+
data: errorData
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Log error
|
|
245
|
+
if (log === true || log === 'errors') {
|
|
246
|
+
console.error(`[JUX Fetch Error] ${requestInit.method || 'GET'} ${fullUrl}`, error);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (onError) {
|
|
250
|
+
onError(error);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
data: null,
|
|
255
|
+
error,
|
|
256
|
+
status: response.status,
|
|
257
|
+
response
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Log success
|
|
262
|
+
if (log === true) {
|
|
263
|
+
console.log(`[JUX Fetch Success] ${requestInit.method || 'GET'} ${fullUrl}`, {
|
|
264
|
+
status: response.status,
|
|
265
|
+
data
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
data,
|
|
271
|
+
error: null,
|
|
272
|
+
status: response.status,
|
|
273
|
+
response
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
} catch (err: any) {
|
|
277
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
278
|
+
|
|
279
|
+
const error: FetchError = {
|
|
280
|
+
message: err.name === 'AbortError'
|
|
281
|
+
? 'Request timeout'
|
|
282
|
+
: err.message || 'Network error',
|
|
283
|
+
status: 0
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Log error
|
|
287
|
+
if (log === true || log === 'errors') {
|
|
288
|
+
console.error(`[JUX Fetch Error] ${requestInit.method || 'GET'} ${fullUrl}`, error);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (onError) {
|
|
292
|
+
onError(error);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
data: null,
|
|
297
|
+
error,
|
|
298
|
+
status: 0,
|
|
299
|
+
response: null as any
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* FetchBuilder class for method chaining
|
|
306
|
+
*/
|
|
307
|
+
class FetchBuilder<T = any> {
|
|
308
|
+
public url: string;
|
|
309
|
+
public options: FetchOptions = {};
|
|
310
|
+
|
|
311
|
+
constructor(url: string, options: FetchOptions = {}) {
|
|
312
|
+
this.url = url;
|
|
313
|
+
this.options = options;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
method(value: string): this {
|
|
317
|
+
this.options.method = value;
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
body(value: any): this {
|
|
322
|
+
this.options.body = value;
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
params(value: Record<string, any>): this {
|
|
327
|
+
this.options.params = value;
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
headers(value: Record<string, string>): this {
|
|
332
|
+
this.options.headers = { ...this.options.headers, ...value };
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
header(key: string, value: string): this {
|
|
337
|
+
this.options.headers = { ...this.options.headers, [key]: value };
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
timeout(value: number): this {
|
|
342
|
+
this.options.timeout = value;
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
credentials(value: RequestCredentials): this {
|
|
347
|
+
this.options.credentials = value;
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
log(value: LogLevel): this {
|
|
352
|
+
this.options.log = value;
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
parseResponse(value: boolean): this {
|
|
357
|
+
this.options.parseResponse = value;
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
onUnauthorized(callback: () => void): this {
|
|
362
|
+
this.options.onUnauthorized = callback;
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
onError(callback: (error: FetchError) => void): this {
|
|
367
|
+
this.options.onError = callback;
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Execute the fetch request
|
|
373
|
+
*/
|
|
374
|
+
send(): Promise<FetchResult<T>> {
|
|
375
|
+
return executeFetch<T>(this.url, this.options);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Alias for send()
|
|
380
|
+
*/
|
|
381
|
+
fetch(): Promise<FetchResult<T>> {
|
|
382
|
+
return this.send();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Make the builder thenable (Promise-like) so it can be awaited directly
|
|
387
|
+
* This allows: await jux.fetch('/users') without needing .send()
|
|
388
|
+
*/
|
|
389
|
+
then<TResult1 = FetchResult<T>, TResult2 = never>(
|
|
390
|
+
onfulfilled?: ((value: FetchResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
|
|
391
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
|
392
|
+
): Promise<TResult1 | TResult2> {
|
|
393
|
+
return this.send().then(onfulfilled, onrejected);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Make the builder catchable for Promise.catch()
|
|
398
|
+
*/
|
|
399
|
+
catch<TResult = never>(
|
|
400
|
+
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
|
401
|
+
): Promise<FetchResult<T> | TResult> {
|
|
402
|
+
return this.send().catch(onrejected);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Make the builder finally-able for Promise.finally()
|
|
407
|
+
*/
|
|
408
|
+
finally(onfinally?: (() => void) | null): Promise<FetchResult<T>> {
|
|
409
|
+
return this.send().finally(onfinally);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Create a fetch builder
|
|
415
|
+
*/
|
|
416
|
+
export function juxFetch<T = any>(url: string, options: FetchOptions = {}): FetchBuilder<T> {
|
|
417
|
+
return new FetchBuilder<T>(url, options);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Convenience methods for common HTTP verbs
|
|
422
|
+
*/
|
|
423
|
+
export const fetchHelpers = {
|
|
424
|
+
get: <T = any>(url: string, options?: Omit<FetchOptions, 'method'>) =>
|
|
425
|
+
new FetchBuilder<T>(url, { ...options, method: 'GET' }),
|
|
426
|
+
|
|
427
|
+
post: <T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>) =>
|
|
428
|
+
new FetchBuilder<T>(url, { ...options, method: 'POST', body }),
|
|
429
|
+
|
|
430
|
+
put: <T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>) =>
|
|
431
|
+
new FetchBuilder<T>(url, { ...options, method: 'PUT', body }),
|
|
432
|
+
|
|
433
|
+
patch: <T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>) =>
|
|
434
|
+
new FetchBuilder<T>(url, { ...options, method: 'PATCH', body }),
|
|
435
|
+
|
|
436
|
+
delete: <T = any>(url: string, options?: Omit<FetchOptions, 'method'>) =>
|
|
437
|
+
new FetchBuilder<T>(url, { ...options, method: 'DELETE' })
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Fetch multiple URLs in parallel
|
|
442
|
+
*/
|
|
443
|
+
export async function fetchAll<T = any>(
|
|
444
|
+
urls: string[],
|
|
445
|
+
options?: FetchOptions
|
|
446
|
+
): Promise<FetchResult<T>[]> {
|
|
447
|
+
return Promise.all(urls.map(url => executeFetch<T>(url, options)));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get a service URL from window.juxConfig
|
|
452
|
+
*/
|
|
453
|
+
export function getServiceUrl(serviceName: string): string | null {
|
|
454
|
+
if (typeof window === 'undefined') return null;
|
|
455
|
+
|
|
456
|
+
const juxConfig = (window as any).juxConfig;
|
|
457
|
+
if (!juxConfig?.services) return null;
|
|
458
|
+
|
|
459
|
+
return juxConfig.services[serviceName] || null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create a fetch instance preconfigured for a specific service
|
|
464
|
+
*
|
|
465
|
+
* Usage:
|
|
466
|
+
* const api = jux.fetch.serviceClient('database');
|
|
467
|
+
* const { data } = await api.fetch('/users').send();
|
|
468
|
+
*/
|
|
469
|
+
export function serviceClient(serviceName: string) {
|
|
470
|
+
const baseUrl = getServiceUrl(serviceName);
|
|
471
|
+
|
|
472
|
+
if (!baseUrl) {
|
|
473
|
+
console.warn(`[JUX Fetch] Service '${serviceName}' not found in juxConfig.services`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
fetch: (url: string, options?: any) => {
|
|
478
|
+
return juxFetch(url, {
|
|
479
|
+
...options,
|
|
480
|
+
baseUrl: baseUrl || undefined
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
baseUrl
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Setup fetch from juxconfig
|
|
489
|
+
* Call this in your bootstrap function
|
|
490
|
+
*
|
|
491
|
+
* Usage:
|
|
492
|
+
* export default {
|
|
493
|
+
* bootstrap: [
|
|
494
|
+
* async function initFetch() {
|
|
495
|
+
* await jux.fetch.setupConfig();
|
|
496
|
+
* }
|
|
497
|
+
* ]
|
|
498
|
+
* };
|
|
499
|
+
*/
|
|
500
|
+
export async function setupConfig() {
|
|
501
|
+
if (typeof window === 'undefined') {
|
|
502
|
+
console.warn('[JUX Fetch] setupConfig() called outside browser environment');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const juxConfig = (window as any).juxConfig;
|
|
507
|
+
|
|
508
|
+
if (!juxConfig) {
|
|
509
|
+
console.warn('[JUX Fetch] No juxConfig found on window. Make sure your juxconfig.js is loaded.');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const services = juxConfig.services || {};
|
|
514
|
+
|
|
515
|
+
if (Object.keys(services).length === 0) {
|
|
516
|
+
console.log('[JUX Fetch] No services configured in juxConfig');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Configure fetch with common defaults
|
|
521
|
+
const fetchConfig: FetchConfig = {
|
|
522
|
+
timeout: juxConfig.fetchTimeout || 30000,
|
|
523
|
+
log: juxConfig.fetchLog || false,
|
|
524
|
+
credentials: 'include'
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// Add service-specific handlers
|
|
528
|
+
if (services.auth) {
|
|
529
|
+
fetchConfig.onUnauthorized = () => {
|
|
530
|
+
console.warn('[JUX Fetch] Unauthorized - consider redirecting to login');
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
configureFetch(fetchConfig);
|
|
535
|
+
|
|
536
|
+
// Log configured services
|
|
537
|
+
console.log('[JUX Fetch] Configured with services:', Object.keys(services));
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
services,
|
|
541
|
+
getServiceUrl
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Export combined API
|
|
546
|
+
export const fetchAPI = Object.assign(juxFetch, {
|
|
547
|
+
config: configureFetch,
|
|
548
|
+
setupConfig,
|
|
549
|
+
getServiceUrl,
|
|
550
|
+
serviceClient,
|
|
551
|
+
...fetchHelpers,
|
|
552
|
+
all: fetchAll
|
|
553
|
+
});
|