@tuyau/core 1.0.0 → 1.2.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/build/backend/generate_registry.d.ts +14 -4
- package/build/backend/generate_registry.js +255 -157
- package/build/client/index.d.ts +27 -29
- package/build/client/index.js +293 -56
- package/build/client/types/index.d.ts +3 -265
- package/build/index-SVztBh2W.d.ts +438 -0
- package/package.json +11 -11
package/build/client/index.js
CHANGED
|
@@ -13,11 +13,18 @@ function buildSearchParams(query) {
|
|
|
13
13
|
const encodedKey = encodeURIComponent(key);
|
|
14
14
|
const fullKey = prefix ? `${prefix}[${encodedKey}]` : encodedKey;
|
|
15
15
|
if (Array.isArray(value)) {
|
|
16
|
-
|
|
17
|
-
if (item
|
|
16
|
+
value.forEach((item, index) => {
|
|
17
|
+
if (item === void 0 || item === null) return;
|
|
18
|
+
if (item instanceof Date) {
|
|
19
|
+
parts.push(`${fullKey}[]=${encodeURIComponent(item.toISOString())}`);
|
|
20
|
+
} else if (typeof item === "object") {
|
|
21
|
+
serialize2(item, `${fullKey}[${index}]`);
|
|
22
|
+
} else {
|
|
18
23
|
parts.push(`${fullKey}[]=${encodeURIComponent(item)}`);
|
|
19
24
|
}
|
|
20
|
-
}
|
|
25
|
+
});
|
|
26
|
+
} else if (value instanceof Date) {
|
|
27
|
+
parts.push(`${fullKey}=${encodeURIComponent(value.toISOString())}`);
|
|
21
28
|
} else if (typeof value === "object") {
|
|
22
29
|
serialize2(value, fullKey);
|
|
23
30
|
} else {
|
|
@@ -29,28 +36,234 @@ function buildSearchParams(query) {
|
|
|
29
36
|
return parts.length ? parts.join("&") : "";
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
// src/client/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return await response.arrayBuffer();
|
|
39
|
+
// src/client/router.ts
|
|
40
|
+
var TuyauRouter = class _TuyauRouter {
|
|
41
|
+
#entries;
|
|
42
|
+
#baseUrl;
|
|
43
|
+
constructor(entries, baseUrl) {
|
|
44
|
+
this.#entries = entries;
|
|
45
|
+
this.#baseUrl = baseUrl;
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Matches a route name against a pattern that may contain `*` wildcards.
|
|
49
|
+
* `*` matches one or more dot-separated segments via recursive backtracking.
|
|
50
|
+
*/
|
|
51
|
+
static matchesRouteNamePattern(routeName, pattern) {
|
|
52
|
+
if (pattern === routeName) return true;
|
|
53
|
+
if (!pattern.includes("*")) return false;
|
|
54
|
+
const patternParts = pattern.split(".");
|
|
55
|
+
const routeParts = routeName.split(".");
|
|
56
|
+
function match(pIdx, rIdx) {
|
|
57
|
+
if (pIdx === patternParts.length) return rIdx === routeParts.length;
|
|
58
|
+
if (patternParts[pIdx] === "*") {
|
|
59
|
+
const remainingPattern = patternParts.length - pIdx - 1;
|
|
60
|
+
for (let i = rIdx + 1; i <= routeParts.length - remainingPattern; i++) {
|
|
61
|
+
if (match(pIdx + 1, i)) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (rIdx >= routeParts.length) return false;
|
|
66
|
+
if (patternParts[pIdx] !== routeParts[rIdx]) return false;
|
|
67
|
+
return match(pIdx + 1, rIdx + 1);
|
|
68
|
+
}
|
|
69
|
+
return match(0, 0);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Matches a URL pathname against an AdonisJS route pattern and extracts
|
|
73
|
+
* dynamic parameters. Supports `:param`, `:param?` (optional), and `*` (wildcard).
|
|
74
|
+
*
|
|
75
|
+
* Returns a record of extracted params on match, or `null` on mismatch.
|
|
76
|
+
*/
|
|
77
|
+
static matchPathAgainstPattern(pathname, pattern) {
|
|
78
|
+
const urlSegments = pathname.split("/").filter(Boolean);
|
|
79
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
80
|
+
const params = {};
|
|
81
|
+
let ui = 0;
|
|
82
|
+
for (const seg of patternSegments) {
|
|
83
|
+
if (seg === "*") {
|
|
84
|
+
if (ui >= urlSegments.length) return null;
|
|
85
|
+
params["*"] = urlSegments.slice(ui).join("/");
|
|
86
|
+
return params;
|
|
87
|
+
}
|
|
88
|
+
if (seg.startsWith(":")) {
|
|
89
|
+
const optional = seg.endsWith("?");
|
|
90
|
+
const name = seg.slice(1, optional ? -1 : void 0);
|
|
91
|
+
if (ui < urlSegments.length) {
|
|
92
|
+
params[name] = urlSegments[ui++];
|
|
93
|
+
} else if (!optional) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ui >= urlSegments.length || urlSegments[ui] !== seg) return null;
|
|
99
|
+
ui++;
|
|
100
|
+
}
|
|
101
|
+
return ui === urlSegments.length ? params : null;
|
|
102
|
+
}
|
|
103
|
+
#getCurrentPathname() {
|
|
104
|
+
if (typeof globalThis.window === "undefined") return void 0;
|
|
105
|
+
const pathname = globalThis.window.location.pathname;
|
|
106
|
+
const basePathname = new URL(this.#baseUrl).pathname;
|
|
107
|
+
if (basePathname !== "/" && pathname.startsWith(basePathname)) {
|
|
108
|
+
return pathname.slice(basePathname.length) || "/";
|
|
109
|
+
}
|
|
110
|
+
return pathname;
|
|
111
|
+
}
|
|
112
|
+
#isNavigableRoute(endpoint) {
|
|
113
|
+
return endpoint.methods.includes("GET") || endpoint.methods.includes("HEAD");
|
|
114
|
+
}
|
|
115
|
+
#findCurrentRouteName(pathname) {
|
|
116
|
+
for (const [name, endpoint] of this.#entries) {
|
|
117
|
+
if (!this.#isNavigableRoute(endpoint)) continue;
|
|
118
|
+
if (_TuyauRouter.matchPathAgainstPattern(pathname, endpoint.pattern)) return name;
|
|
119
|
+
}
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
#matchesCurrentRoute(pathname, routeName, options) {
|
|
123
|
+
for (const [name, endpoint] of this.#entries) {
|
|
124
|
+
if (!this.#isNavigableRoute(endpoint)) continue;
|
|
125
|
+
if (!_TuyauRouter.matchesRouteNamePattern(name, routeName)) continue;
|
|
126
|
+
const params = _TuyauRouter.matchPathAgainstPattern(pathname, endpoint.pattern);
|
|
127
|
+
if (!params) continue;
|
|
128
|
+
if (options?.params) {
|
|
129
|
+
const mismatch = Object.entries(options.params).some(
|
|
130
|
+
([key, value]) => params[key] !== String(value)
|
|
131
|
+
);
|
|
132
|
+
if (mismatch) continue;
|
|
133
|
+
}
|
|
134
|
+
if (options?.query) {
|
|
135
|
+
if (!this.#matchesQueryString(globalThis.window.location.search, options.query)) continue;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
#parseQueryString(search) {
|
|
142
|
+
if (!search || search === "?") return {};
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const [key, value] of new URLSearchParams(search)) {
|
|
145
|
+
const current = result[key];
|
|
146
|
+
if (current === void 0) {
|
|
147
|
+
result[key] = value;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
result[key] = Array.isArray(current) ? [...current, value] : [current, value];
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
#matchesQueryString(search, query) {
|
|
155
|
+
const currentQuery = this.#parseQueryString(search);
|
|
156
|
+
const expectedQuery = this.#parseQueryString(buildSearchParams(query));
|
|
157
|
+
return Object.entries(expectedQuery).every(([key, value]) => {
|
|
158
|
+
const currentValue = currentQuery[key];
|
|
159
|
+
if (currentValue === void 0) return false;
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
return Array.isArray(currentValue) ? value.length === currentValue.length && value.every((entry, index) => currentValue[index] === entry) : value.length === 1 && currentValue === value[0];
|
|
162
|
+
}
|
|
163
|
+
return Array.isArray(currentValue) ? currentValue.length === 1 && currentValue[0] === value : currentValue === value;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Checks if a route name exists in the registry.
|
|
168
|
+
*/
|
|
169
|
+
has(routeName) {
|
|
170
|
+
return this.#entries.some(([name]) => _TuyauRouter.matchesRouteNamePattern(name, routeName));
|
|
171
|
+
}
|
|
172
|
+
current(routeName, options) {
|
|
173
|
+
const pathname = this.#getCurrentPathname();
|
|
174
|
+
if (!pathname) return routeName ? false : void 0;
|
|
175
|
+
if (!routeName) return this.#findCurrentRouteName(pathname);
|
|
176
|
+
return this.#matchesCurrentRoute(pathname, routeName, options);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/client/promise.ts
|
|
181
|
+
var TuyauPromise = class {
|
|
182
|
+
#promise;
|
|
183
|
+
constructor(promise) {
|
|
184
|
+
this.#promise = promise;
|
|
185
|
+
}
|
|
186
|
+
then(onfulfilled, onrejected) {
|
|
187
|
+
return this.#promise.then(onfulfilled, onrejected);
|
|
188
|
+
}
|
|
189
|
+
catch(onrejected) {
|
|
190
|
+
return this.#promise.catch(onrejected);
|
|
191
|
+
}
|
|
192
|
+
finally(onfinally) {
|
|
193
|
+
return this.#promise.finally(onfinally);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Returns a tuple instead of throwing on error.
|
|
197
|
+
* - On success: `[data, null]`
|
|
198
|
+
* - On error: `[null, TuyauError]`
|
|
199
|
+
*/
|
|
200
|
+
safe() {
|
|
201
|
+
return this.#promise.then(
|
|
202
|
+
(data) => [data, null],
|
|
203
|
+
(error) => [null, error]
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/client/errors.ts
|
|
209
|
+
var TuyauError = class extends Error {
|
|
210
|
+
/**
|
|
211
|
+
* Error kind exposed by Tuyau.
|
|
212
|
+
* - `'http'`: the server responded with a non-2xx status code.
|
|
213
|
+
* - `'network'`: the request failed before receiving a response.
|
|
214
|
+
*/
|
|
215
|
+
kind;
|
|
216
|
+
/**
|
|
217
|
+
* HTTP status code returned by the server.
|
|
218
|
+
* `undefined` when the request failed before a response was received.
|
|
219
|
+
*/
|
|
44
220
|
status;
|
|
221
|
+
/**
|
|
222
|
+
* Original Ky response object for HTTP errors.
|
|
223
|
+
* `undefined` for network errors.
|
|
224
|
+
*/
|
|
45
225
|
rawResponse;
|
|
226
|
+
/**
|
|
227
|
+
* Original Ky request object when available.
|
|
228
|
+
*/
|
|
46
229
|
rawRequest;
|
|
230
|
+
/**
|
|
231
|
+
* Parsed error payload returned by the server.
|
|
232
|
+
* `undefined` for network errors.
|
|
233
|
+
*/
|
|
47
234
|
response;
|
|
235
|
+
constructor(message, options) {
|
|
236
|
+
super(message, options);
|
|
237
|
+
this.status = void 0;
|
|
238
|
+
this.response = void 0;
|
|
239
|
+
this.rawResponse = void 0;
|
|
240
|
+
this.rawRequest = void 0;
|
|
241
|
+
this.name = "TuyauError";
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Type guard that narrows the error to a specific HTTP status code.
|
|
245
|
+
* After calling `isStatus(422)`, the `response` property is narrowed
|
|
246
|
+
* to the type associated with that status code.
|
|
247
|
+
*/
|
|
248
|
+
isStatus(status) {
|
|
249
|
+
return this.kind === "http" && this.status === status;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Type guard that narrows to a 422 validation error.
|
|
253
|
+
* Alias for `isStatus(422)`.
|
|
254
|
+
*/
|
|
255
|
+
isValidationError() {
|
|
256
|
+
return this.isStatus(422);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var TuyauHTTPError = class extends TuyauError {
|
|
48
260
|
constructor(kyError, response) {
|
|
49
261
|
const status = kyError.response?.status;
|
|
50
262
|
const reason = status ? `status code ${status}` : "an unknown error";
|
|
51
263
|
const url = kyError.request?.url.replace(kyError.options.prefixUrl || "", "");
|
|
52
264
|
const message = kyError.response ? `Request failed with ${reason}: ${kyError.request?.method.toUpperCase()} /${url}` : `Request failed with ${reason}`;
|
|
53
265
|
super(message, { cause: kyError });
|
|
266
|
+
this.kind = "http";
|
|
54
267
|
this.rawResponse = kyError.response;
|
|
55
268
|
this.response = response;
|
|
56
269
|
this.status = kyError.response?.status;
|
|
@@ -58,12 +271,11 @@ var TuyauHTTPError = class extends Error {
|
|
|
58
271
|
this.name = "TuyauHTTPError";
|
|
59
272
|
}
|
|
60
273
|
};
|
|
61
|
-
var TuyauNetworkError = class extends
|
|
62
|
-
cause;
|
|
274
|
+
var TuyauNetworkError = class extends TuyauError {
|
|
63
275
|
constructor(cause, request) {
|
|
64
276
|
const message = request ? `Network error: ${request.method.toUpperCase()} ${request.url}` : "Network error occurred";
|
|
65
277
|
super(message, { cause });
|
|
66
|
-
this.
|
|
278
|
+
this.kind = "network";
|
|
67
279
|
this.name = "TuyauNetworkError";
|
|
68
280
|
}
|
|
69
281
|
};
|
|
@@ -83,6 +295,16 @@ function segmentsToRouteName(segments) {
|
|
|
83
295
|
function segmentsToKebabRouteName(segments) {
|
|
84
296
|
return segments.map((segment) => segment.replace(/[A-Z]/g, (g) => `-${g.toLowerCase()}`)).join(".");
|
|
85
297
|
}
|
|
298
|
+
async function parseResponse(response) {
|
|
299
|
+
if (!response) return;
|
|
300
|
+
const responseType = response.headers.get("Content-Type")?.split(";")[0];
|
|
301
|
+
if (responseType === "application/json") {
|
|
302
|
+
return await response.json();
|
|
303
|
+
} else if (responseType === "application/octet-stream") {
|
|
304
|
+
return await response.arrayBuffer();
|
|
305
|
+
}
|
|
306
|
+
return await response.text();
|
|
307
|
+
}
|
|
86
308
|
|
|
87
309
|
// src/client/tuyau.ts
|
|
88
310
|
var Tuyau = class {
|
|
@@ -91,6 +313,7 @@ var Tuyau = class {
|
|
|
91
313
|
#entries;
|
|
92
314
|
#client;
|
|
93
315
|
#config;
|
|
316
|
+
#router;
|
|
94
317
|
/**
|
|
95
318
|
* Merges the default Ky configuration with user-provided config
|
|
96
319
|
*/
|
|
@@ -128,6 +351,7 @@ var Tuyau = class {
|
|
|
128
351
|
this.api = this.#makeNamed([]);
|
|
129
352
|
this.#entries = Object.entries(this.#config.registry.routes);
|
|
130
353
|
this.urlFor = this.#createUrlBuilder();
|
|
354
|
+
this.#router = new TuyauRouter(this.#entries, this.#config.baseUrl);
|
|
131
355
|
this.#applyPlugins();
|
|
132
356
|
this.#client = ky.create(this.#mergeKyConfiguration());
|
|
133
357
|
}
|
|
@@ -174,45 +398,48 @@ var Tuyau = class {
|
|
|
174
398
|
/**
|
|
175
399
|
* Performs the actual HTTP request with proper body formatting
|
|
176
400
|
*/
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const isGetOrHead = ["GET", "HEAD"].includes(method);
|
|
188
|
-
const { body: _, ...restArgs } = args;
|
|
189
|
-
const requestOptions = {
|
|
190
|
-
searchParams: buildSearchParams(args?.query || {}),
|
|
191
|
-
[key]: !isGetOrHead ? body : void 0,
|
|
192
|
-
...restArgs
|
|
193
|
-
};
|
|
194
|
-
try {
|
|
195
|
-
const res = await this.#client[this.#getLowercaseMethod(method)](
|
|
196
|
-
removeSlash(url),
|
|
197
|
-
requestOptions
|
|
198
|
-
);
|
|
199
|
-
let data;
|
|
200
|
-
const responseType = res.headers.get("Content-Type")?.split(";")[0]?.trim();
|
|
201
|
-
if (responseType === "application/json") {
|
|
202
|
-
data = await res.json();
|
|
203
|
-
} else if (responseType === "application/octet-stream") {
|
|
204
|
-
data = await res.arrayBuffer();
|
|
205
|
-
} else {
|
|
206
|
-
data = await res.text();
|
|
401
|
+
#doFetch(name, method, args) {
|
|
402
|
+
const promise = (async () => {
|
|
403
|
+
const url = this.#buildUrl(name, method, args);
|
|
404
|
+
let key = "json";
|
|
405
|
+
let body = args.body;
|
|
406
|
+
if (!(body instanceof FormData) && this.#hasFile(body)) {
|
|
407
|
+
body = serialize(body, { indices: true });
|
|
408
|
+
key = "body";
|
|
409
|
+
} else if (body instanceof FormData) {
|
|
410
|
+
key = "body";
|
|
207
411
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
412
|
+
const isGetOrHead = ["GET", "HEAD"].includes(method);
|
|
413
|
+
const { body: _, responseType: requestedResponseType, ...restArgs } = args;
|
|
414
|
+
const requestOptions = {
|
|
415
|
+
searchParams: buildSearchParams(args?.query || {}),
|
|
416
|
+
[key]: !isGetOrHead ? body : void 0,
|
|
417
|
+
...restArgs
|
|
418
|
+
};
|
|
419
|
+
try {
|
|
420
|
+
const res = await this.#client[this.#getLowercaseMethod(method)](
|
|
421
|
+
removeSlash(url),
|
|
422
|
+
requestOptions
|
|
423
|
+
);
|
|
424
|
+
let data;
|
|
425
|
+
if (requestedResponseType) {
|
|
426
|
+
data = await res[requestedResponseType]();
|
|
427
|
+
} else {
|
|
428
|
+
const contentType = res.headers.get("Content-Type")?.split(";")[0]?.trim();
|
|
429
|
+
if (contentType === "application/json") data = await res.json();
|
|
430
|
+
else if (contentType === "application/octet-stream") data = await res.arrayBuffer();
|
|
431
|
+
else data = await res.text();
|
|
432
|
+
}
|
|
433
|
+
return data;
|
|
434
|
+
} catch (originalError) {
|
|
435
|
+
if (originalError instanceof HTTPError) {
|
|
436
|
+
const parsedResponse = await parseResponse(originalError.response);
|
|
437
|
+
throw new TuyauHTTPError(originalError, parsedResponse);
|
|
438
|
+
}
|
|
439
|
+
throw new TuyauNetworkError(originalError, { url, method });
|
|
213
440
|
}
|
|
214
|
-
|
|
215
|
-
|
|
441
|
+
})();
|
|
442
|
+
return new TuyauPromise(promise);
|
|
216
443
|
}
|
|
217
444
|
/**
|
|
218
445
|
* Finds an endpoint definition by HTTP method and pattern
|
|
@@ -302,14 +529,24 @@ var Tuyau = class {
|
|
|
302
529
|
}
|
|
303
530
|
return new Proxy({}, { get: (_t, prop) => this.#makeNamed([...segments, String(prop)]) });
|
|
304
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Checks if a route name exists in the registry.
|
|
534
|
+
*/
|
|
535
|
+
has(routeName) {
|
|
536
|
+
return this.#router.has(routeName);
|
|
537
|
+
}
|
|
538
|
+
current(routeName, options) {
|
|
539
|
+
return this.#router.current(routeName, options);
|
|
540
|
+
}
|
|
305
541
|
};
|
|
306
542
|
function createTuyau(config) {
|
|
307
543
|
return new Tuyau(config);
|
|
308
544
|
}
|
|
309
545
|
export {
|
|
310
546
|
Tuyau,
|
|
547
|
+
TuyauError,
|
|
311
548
|
TuyauHTTPError,
|
|
312
549
|
TuyauNetworkError,
|
|
313
|
-
|
|
314
|
-
|
|
550
|
+
TuyauPromise,
|
|
551
|
+
createTuyau
|
|
315
552
|
};
|