@tuyau/core 0.4.1 → 1.0.0-beta.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 +182 -0
- package/build/backend/generate_registry.js +88 -0
- package/build/client/index.d.ts +82 -0
- package/build/client/index.js +291 -0
- package/build/client/types/index.d.ts +195 -0
- package/build/commands/commands.json +1 -1
- package/package.json +30 -24
- package/build/chunk-ADS4GRIL.js +0 -14
- package/build/commands/generate.d.ts +0 -16
- package/build/commands/generate.js +0 -362
- package/build/config/tuyau.stub +0 -20
- package/build/index.d.ts +0 -9
- package/build/index.js +0 -30
- package/build/providers/tuyau_provider.d.ts +0 -190
- package/build/providers/tuyau_provider.js +0 -13
- package/build/src/hooks/build_hook.d.ts +0 -5
- package/build/src/hooks/build_hook.js +0 -14
- package/build/src/types.d.ts +0 -25
- /package/build/{src/types.js → client/types/index.js} +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as _adonisjs_assembler_routes_scanner from '@adonisjs/assembler/routes_scanner';
|
|
2
|
+
import * as _poppinss_cliui_types from '@poppinss/cliui/types';
|
|
3
|
+
import * as _poppinss_cliui from '@poppinss/cliui';
|
|
4
|
+
import * as _poppinss_colors_types from '@poppinss/colors/types';
|
|
5
|
+
import { DevServerOptions } from './types/common.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Exposes the API to start the development server in HMR, watch or static mode
|
|
9
|
+
*
|
|
10
|
+
* In HMR mode, the DevServer will exec the "bin/server.ts" file and let hot-hook
|
|
11
|
+
* manage the changes using hot module reloading.
|
|
12
|
+
*
|
|
13
|
+
* In watch mode, the DevServer will start an internal watcher and restarts the server
|
|
14
|
+
* after every file change. The files must be part of the TypeScript project (via tsconfig.json),
|
|
15
|
+
* or registered as metaFiles.
|
|
16
|
+
*
|
|
17
|
+
* In static mode, the server runs without file watching or hot reloading.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const devServer = new DevServer(cwd, { hmr: true, hooks: [] })
|
|
21
|
+
* await devServer.start(ts)
|
|
22
|
+
*/
|
|
23
|
+
declare class DevServer {
|
|
24
|
+
#private;
|
|
25
|
+
/**
|
|
26
|
+
* CLI UI instance for displaying colorful messages and progress information
|
|
27
|
+
*/
|
|
28
|
+
ui: {
|
|
29
|
+
colors: _poppinss_colors_types.Colors;
|
|
30
|
+
logger: _poppinss_cliui.Logger;
|
|
31
|
+
table: (tableOptions?: Partial<_poppinss_cliui_types.TableOptions>) => _poppinss_cliui.Table;
|
|
32
|
+
tasks: (tasksOptions?: Partial<_poppinss_cliui_types.TaskManagerOptions>) => _poppinss_cliui.TaskManager;
|
|
33
|
+
icons: {
|
|
34
|
+
tick: string;
|
|
35
|
+
cross: string;
|
|
36
|
+
bullet: string;
|
|
37
|
+
nodejs: string;
|
|
38
|
+
pointer: string;
|
|
39
|
+
info: string;
|
|
40
|
+
warning: string;
|
|
41
|
+
squareSmallFilled: string;
|
|
42
|
+
};
|
|
43
|
+
sticker: () => _poppinss_cliui.Instructions;
|
|
44
|
+
instructions: () => _poppinss_cliui.Instructions;
|
|
45
|
+
switchMode(modeToUse: "raw" | "silent" | "normal"): void;
|
|
46
|
+
useRenderer(rendererToUse: _poppinss_cliui_types.RendererContract): void;
|
|
47
|
+
useColors(colorsToUse: _poppinss_colors_types.Colors): void;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The mode in which the DevServer is running
|
|
51
|
+
*
|
|
52
|
+
* Returns the current operating mode of the development server:
|
|
53
|
+
* - 'hmr': Hot Module Reloading enabled
|
|
54
|
+
* - 'watch': File system watching with full restarts
|
|
55
|
+
* - 'static': No file watching or hot reloading
|
|
56
|
+
*/
|
|
57
|
+
get mode(): "hmr" | "watch" | "static";
|
|
58
|
+
/**
|
|
59
|
+
* Script file to start the development server
|
|
60
|
+
*/
|
|
61
|
+
scriptFile: string;
|
|
62
|
+
/**
|
|
63
|
+
* The current working directory URL
|
|
64
|
+
*/
|
|
65
|
+
cwd: URL;
|
|
66
|
+
/**
|
|
67
|
+
* File path computed from the cwd
|
|
68
|
+
*/
|
|
69
|
+
cwdPath: string;
|
|
70
|
+
/**
|
|
71
|
+
* Development server configuration options including hooks and environment variables
|
|
72
|
+
*/
|
|
73
|
+
options: DevServerOptions;
|
|
74
|
+
/**
|
|
75
|
+
* Create a new DevServer instance
|
|
76
|
+
*
|
|
77
|
+
* @param cwd - The current working directory URL
|
|
78
|
+
* @param options - Development server configuration options
|
|
79
|
+
*/
|
|
80
|
+
constructor(cwd: URL, options: DevServerOptions);
|
|
81
|
+
/**
|
|
82
|
+
* Adds listener to get notified when dev server is closed
|
|
83
|
+
*
|
|
84
|
+
* Registers a callback function that will be invoked when the development
|
|
85
|
+
* server's child process exits. The callback receives the exit code.
|
|
86
|
+
*
|
|
87
|
+
* @param callback - Function to call when dev server closes
|
|
88
|
+
* @returns This DevServer instance for method chaining
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* devServer.onClose((exitCode) => {
|
|
92
|
+
* console.log(`Server closed with exit code: ${exitCode}`)
|
|
93
|
+
* })
|
|
94
|
+
*/
|
|
95
|
+
onClose(callback: (exitCode: number) => any): this;
|
|
96
|
+
/**
|
|
97
|
+
* Adds listener to get notified when dev server encounters an error
|
|
98
|
+
*
|
|
99
|
+
* Registers a callback function that will be invoked when the development
|
|
100
|
+
* server's child process encounters an error or fails to start.
|
|
101
|
+
*
|
|
102
|
+
* @param callback - Function to call when dev server encounters an error
|
|
103
|
+
* @returns This DevServer instance for method chaining
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* devServer.onError((error) => {
|
|
107
|
+
* console.error('Dev server error:', error.message)
|
|
108
|
+
* })
|
|
109
|
+
*/
|
|
110
|
+
onError(callback: (error: any) => any): this;
|
|
111
|
+
/**
|
|
112
|
+
* Closes watchers and terminates the running child process
|
|
113
|
+
*
|
|
114
|
+
* Cleans up keyboard shortcuts, stops file system watchers, and kills
|
|
115
|
+
* the HTTP server child process. This should be called when shutting down
|
|
116
|
+
* the development server.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* await devServer.close()
|
|
120
|
+
*/
|
|
121
|
+
close(): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Starts the development server in static or HMR mode
|
|
124
|
+
*
|
|
125
|
+
* Initializes the server and starts the HTTP server. The mode is determined
|
|
126
|
+
* by the `hmr` option in DevServerOptions. In HMR mode, hot-hook is configured
|
|
127
|
+
* to enable hot module reloading.
|
|
128
|
+
*
|
|
129
|
+
* @param ts - TypeScript module reference
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* const devServer = new DevServer(cwd, { hmr: true, hooks: [] })
|
|
133
|
+
* await devServer.start(ts)
|
|
134
|
+
*/
|
|
135
|
+
start(): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Starts the development server in watch mode and restarts on file changes
|
|
138
|
+
*
|
|
139
|
+
* Initializes the server, starts the HTTP server, and sets up a file system
|
|
140
|
+
* watcher that monitors for changes. When files are added, modified, or deleted,
|
|
141
|
+
* the server automatically restarts. The watcher respects TypeScript project
|
|
142
|
+
* configuration and metaFiles settings.
|
|
143
|
+
*
|
|
144
|
+
* @param ts - TypeScript module reference
|
|
145
|
+
* @param options - Watch options including polling mode
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* const devServer = new DevServer(cwd, { hooks: [] })
|
|
149
|
+
* await devServer.startAndWatch(ts, { poll: false })
|
|
150
|
+
*/
|
|
151
|
+
startAndWatch(options?: {
|
|
152
|
+
poll: boolean;
|
|
153
|
+
}): Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface GenerateRegistryConfig {
|
|
157
|
+
/**
|
|
158
|
+
* Path to write the generated registry file
|
|
159
|
+
* @default ./.adonisjs/client/registry.ts
|
|
160
|
+
*/
|
|
161
|
+
output?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Routes filtering configuration
|
|
164
|
+
*/
|
|
165
|
+
routes?: {
|
|
166
|
+
/**
|
|
167
|
+
* Only include routes matching these patterns (route names)
|
|
168
|
+
* Can be strings, regex patterns, or functions
|
|
169
|
+
*/
|
|
170
|
+
only?: Array<string | RegExp | ((routeName: string) => boolean)>;
|
|
171
|
+
/**
|
|
172
|
+
* Exclude routes matching these patterns (route names)
|
|
173
|
+
* Can be strings, regex patterns, or functions
|
|
174
|
+
*/
|
|
175
|
+
except?: Array<string | RegExp | ((routeName: string) => boolean)>;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
declare function generateRegistry(options?: GenerateRegistryConfig): {
|
|
179
|
+
run(devServer: DevServer, routesScanner: _adonisjs_assembler_routes_scanner.RoutesScanner): Promise<void>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export { generateRegistry };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/backend/generate_registry.ts
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import string from "@adonisjs/core/helpers/string";
|
|
4
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
5
|
+
async function writeOutputFile(filePath, content) {
|
|
6
|
+
const dir = dirname(filePath);
|
|
7
|
+
await mkdir(dir, { recursive: true });
|
|
8
|
+
await writeFile(filePath, content);
|
|
9
|
+
}
|
|
10
|
+
function matchesPattern(routeName, pattern) {
|
|
11
|
+
if (typeof pattern === "string") return routeName.includes(pattern);
|
|
12
|
+
if (pattern instanceof RegExp) return pattern.test(routeName);
|
|
13
|
+
if (typeof pattern === "function") return pattern(routeName);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function filterRoutes(routes, filters) {
|
|
17
|
+
if (!filters) return routes;
|
|
18
|
+
const { only, except } = filters;
|
|
19
|
+
if (only && except)
|
|
20
|
+
throw new Error('Cannot use both "only" and "except" filters at the same time');
|
|
21
|
+
if (only) {
|
|
22
|
+
return routes.filter((route) => only.some((pattern) => matchesPattern(route.name, pattern)));
|
|
23
|
+
}
|
|
24
|
+
if (except) {
|
|
25
|
+
return routes.filter((route) => !except.some((pattern) => matchesPattern(route.name, pattern)));
|
|
26
|
+
}
|
|
27
|
+
return routes;
|
|
28
|
+
}
|
|
29
|
+
function generateRouteParams(route) {
|
|
30
|
+
const dynamicParams = route.tokens.filter((token) => token.type === 1);
|
|
31
|
+
const paramsType = dynamicParams.map((token) => `${token.val}: string`).join("; ");
|
|
32
|
+
const paramsTuple = dynamicParams.map(() => "string").join(", ");
|
|
33
|
+
return { paramsType, paramsTuple };
|
|
34
|
+
}
|
|
35
|
+
function generateRegistryEntry(route) {
|
|
36
|
+
const requestType = route.request?.type || "{}";
|
|
37
|
+
const responseType = route.response?.type || "unknown";
|
|
38
|
+
const { paramsType, paramsTuple } = generateRouteParams(route);
|
|
39
|
+
const routeName = route.name.split(".").map((segment) => string.camelCase(segment)).join(".");
|
|
40
|
+
return ` '${routeName}': {
|
|
41
|
+
methods: ${JSON.stringify(route.methods)},
|
|
42
|
+
pattern: '${route.pattern}',
|
|
43
|
+
tokens: ${JSON.stringify(route.tokens)},
|
|
44
|
+
types: placeholder as {
|
|
45
|
+
body: ${requestType}
|
|
46
|
+
paramsTuple: [${paramsTuple}]
|
|
47
|
+
params: ${paramsType ? `{ ${paramsType} }` : "{}"}
|
|
48
|
+
query: {}
|
|
49
|
+
response: ${responseType}
|
|
50
|
+
},
|
|
51
|
+
}`;
|
|
52
|
+
}
|
|
53
|
+
function generateRegistryContent(routes) {
|
|
54
|
+
const registryEntries = routes.map(generateRegistryEntry).join(",\n");
|
|
55
|
+
return `/* eslint-disable prettier/prettier */
|
|
56
|
+
import type { AdonisEndpoint } from '@tuyau/core/types'
|
|
57
|
+
import type { Infer } from '@vinejs/vine/types'
|
|
58
|
+
|
|
59
|
+
const placeholder: any = {}
|
|
60
|
+
export const registry = {
|
|
61
|
+
${registryEntries}
|
|
62
|
+
} as const satisfies Record<string, AdonisEndpoint>
|
|
63
|
+
|
|
64
|
+
declare module '@tuyau/core/types' {
|
|
65
|
+
type Registry = typeof registry
|
|
66
|
+
export interface UserRegistry extends Registry {}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function generateRegistry(options) {
|
|
71
|
+
const config = {
|
|
72
|
+
output: "./.adonisjs/client/registry.ts",
|
|
73
|
+
...options
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
async run(devServer, routesScanner) {
|
|
77
|
+
const startTime = process.hrtime();
|
|
78
|
+
const scannedRoutes = routesScanner.getScannedRoutes();
|
|
79
|
+
const filteredRoutes = filterRoutes(scannedRoutes, config.routes);
|
|
80
|
+
const registryContent = generateRegistryContent(filteredRoutes);
|
|
81
|
+
await writeOutputFile(config.output, registryContent);
|
|
82
|
+
devServer.ui.logger.info(`created ${config.output}`, { startTime });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
generateRegistry
|
|
88
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { UrlFor } from '@adonisjs/http-server/client/url_builder';
|
|
2
|
+
import { AdonisEndpoint, TuyauConfiguration, BuildNamed, RegistryGroupedByMethod, PatternsByMethod, RequestArgs, EndpointByMethodPattern, StrKeys, Method } from './types/index.js';
|
|
3
|
+
import { KyResponse, KyRequest, HTTPError } from 'ky';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Main client class for making HTTP requests to AdonisJS endpoints
|
|
7
|
+
* Provides both fluent API and direct method calling capabilities
|
|
8
|
+
*/
|
|
9
|
+
declare class Tuyau<R extends Record<string, AdonisEndpoint>> {
|
|
10
|
+
#private;
|
|
11
|
+
private config;
|
|
12
|
+
readonly api: BuildNamed<R>;
|
|
13
|
+
readonly urlFor: UrlFor<RegistryGroupedByMethod<R>>;
|
|
14
|
+
/**
|
|
15
|
+
* Initializes the Tuyau client with provided configuration
|
|
16
|
+
*/
|
|
17
|
+
constructor(config: TuyauConfiguration<R>);
|
|
18
|
+
/**
|
|
19
|
+
* Makes a GET request to the specified pattern
|
|
20
|
+
*/
|
|
21
|
+
get<P extends PatternsByMethod<R, 'GET'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'GET', P>>): Promise<EndpointByMethodPattern<R, 'GET', P>['types']['response']>;
|
|
22
|
+
/**
|
|
23
|
+
* Makes a POST request to the specified pattern
|
|
24
|
+
*/
|
|
25
|
+
post<P extends PatternsByMethod<R, 'POST'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'POST', P>>): Promise<EndpointByMethodPattern<R, 'POST', P>['types']['response']>;
|
|
26
|
+
/**
|
|
27
|
+
* Makes a PUT request to the specified pattern
|
|
28
|
+
*/
|
|
29
|
+
put<P extends PatternsByMethod<R, 'PUT'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'PUT', P>>): Promise<EndpointByMethodPattern<R, 'PUT', P>['types']['response']>;
|
|
30
|
+
/**
|
|
31
|
+
* Makes a PATCH request to the specified pattern
|
|
32
|
+
*/
|
|
33
|
+
patch<P extends PatternsByMethod<R, 'PATCH'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'PATCH', P>>): Promise<EndpointByMethodPattern<R, 'PATCH', P>['types']['response']>;
|
|
34
|
+
/**
|
|
35
|
+
* Makes a DELETE request to the specified pattern
|
|
36
|
+
*/
|
|
37
|
+
delete<P extends PatternsByMethod<R, 'DELETE'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'DELETE', P>>): Promise<EndpointByMethodPattern<R, 'DELETE', P>['types']['response']>;
|
|
38
|
+
/**
|
|
39
|
+
* Makes a HEAD request to the specified pattern
|
|
40
|
+
*/
|
|
41
|
+
head<P extends PatternsByMethod<R, 'HEAD'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'HEAD', P>>): Promise<EndpointByMethodPattern<R, 'HEAD', P>['types']['response']>;
|
|
42
|
+
/**
|
|
43
|
+
* Makes a request to a named endpoint
|
|
44
|
+
*/
|
|
45
|
+
request<Name extends StrKeys<R>>(name: Name, args: RequestArgs<R[Name]>): Promise<R[Name]['types']['response']>;
|
|
46
|
+
/**
|
|
47
|
+
* Gets route information by name including URL and HTTP method
|
|
48
|
+
*/
|
|
49
|
+
getRoute<Name extends StrKeys<R>>(name: Name, args: RequestArgs<R[Name]>): {
|
|
50
|
+
url: string;
|
|
51
|
+
methods: Method[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Creates a proxy-based fluent API for accessing endpoints by name
|
|
55
|
+
*/
|
|
56
|
+
private makeNamed;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Factory function to create a new Tuyau client instance
|
|
60
|
+
*/
|
|
61
|
+
declare function createTuyau<R extends Record<string, AdonisEndpoint>>(config: TuyauConfiguration<R>): Tuyau<R>;
|
|
62
|
+
|
|
63
|
+
declare function parseResponse(response?: KyResponse): Promise<unknown>;
|
|
64
|
+
declare class TuyauHTTPError extends Error {
|
|
65
|
+
status: number | undefined;
|
|
66
|
+
rawResponse: KyResponse | undefined;
|
|
67
|
+
rawRequest: KyRequest | undefined;
|
|
68
|
+
response: any;
|
|
69
|
+
constructor(kyError: HTTPError, response: any);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Network error that occurs when the server is unreachable or the client is offline
|
|
73
|
+
*/
|
|
74
|
+
declare class TuyauNetworkError extends Error {
|
|
75
|
+
cause: Error;
|
|
76
|
+
constructor(cause: Error, request?: {
|
|
77
|
+
url: string;
|
|
78
|
+
method: string;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { Tuyau, TuyauHTTPError, TuyauNetworkError, createTuyau, parseResponse };
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// src/client/tuyau.ts
|
|
2
|
+
import ky, { HTTPError } from "ky";
|
|
3
|
+
import { serialize } from "object-to-formdata";
|
|
4
|
+
import { createUrlBuilder } from "@adonisjs/http-server/client/url_builder";
|
|
5
|
+
|
|
6
|
+
// src/client/errors.ts
|
|
7
|
+
async function parseResponse(response) {
|
|
8
|
+
if (!response) return;
|
|
9
|
+
const responseType = response.headers.get("Content-Type")?.split(";")[0];
|
|
10
|
+
if (responseType === "application/json") {
|
|
11
|
+
return await response.json();
|
|
12
|
+
} else if (responseType === "application/octet-stream") {
|
|
13
|
+
return await response.arrayBuffer();
|
|
14
|
+
}
|
|
15
|
+
return await response.text();
|
|
16
|
+
}
|
|
17
|
+
var TuyauHTTPError = class extends Error {
|
|
18
|
+
status;
|
|
19
|
+
rawResponse;
|
|
20
|
+
rawRequest;
|
|
21
|
+
response;
|
|
22
|
+
constructor(kyError, response) {
|
|
23
|
+
const status = kyError.response?.status;
|
|
24
|
+
const reason = status ? `status code ${status}` : "an unknown error";
|
|
25
|
+
const url = kyError.request?.url.replace(kyError.options.prefixUrl || "", "");
|
|
26
|
+
const message = kyError.response ? `Request failed with ${reason}: ${kyError.request?.method.toUpperCase()} /${url}` : `Request failed with ${reason}`;
|
|
27
|
+
super(message, { cause: kyError });
|
|
28
|
+
this.rawResponse = kyError.response;
|
|
29
|
+
this.response = response;
|
|
30
|
+
this.status = kyError.response?.status;
|
|
31
|
+
this.rawRequest = kyError.request;
|
|
32
|
+
this.name = "TuyauHTTPError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var TuyauNetworkError = class extends Error {
|
|
36
|
+
constructor(cause, request) {
|
|
37
|
+
const message = request ? `Network error: ${request.method.toUpperCase()} ${request.url}` : "Network error occurred";
|
|
38
|
+
super(message, { cause });
|
|
39
|
+
this.cause = cause;
|
|
40
|
+
this.name = "TuyauNetworkError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/client/utils.ts
|
|
45
|
+
function buildSearchParams(query) {
|
|
46
|
+
if (!query) return;
|
|
47
|
+
let stringified = "";
|
|
48
|
+
const append = (key, value, isArray = false) => {
|
|
49
|
+
if (value === void 0 || value === null) return;
|
|
50
|
+
const encodedKey = encodeURIComponent(key);
|
|
51
|
+
const encodedValue = encodeURIComponent(value);
|
|
52
|
+
const keyValuePair = `${encodedKey}${isArray ? "[]" : ""}=${encodedValue}`;
|
|
53
|
+
stringified += (stringified ? "&" : "?") + keyValuePair;
|
|
54
|
+
};
|
|
55
|
+
for (const [key, value] of Object.entries(query)) {
|
|
56
|
+
if (!value) continue;
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
for (const v of value) append(key, v, true);
|
|
59
|
+
} else {
|
|
60
|
+
append(key, `${value}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return stringified;
|
|
64
|
+
}
|
|
65
|
+
function removeSlash(value) {
|
|
66
|
+
return value.replace(/^\//, "");
|
|
67
|
+
}
|
|
68
|
+
function isObject(value) {
|
|
69
|
+
return typeof value === "object" && !Array.isArray(value) && value !== null;
|
|
70
|
+
}
|
|
71
|
+
var isServer = typeof FileList === "undefined";
|
|
72
|
+
var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
|
73
|
+
|
|
74
|
+
// src/client/tuyau.ts
|
|
75
|
+
var Tuyau = class {
|
|
76
|
+
/**
|
|
77
|
+
* Initializes the Tuyau client with provided configuration
|
|
78
|
+
*/
|
|
79
|
+
constructor(config) {
|
|
80
|
+
this.config = config;
|
|
81
|
+
this.api = this.makeNamed([]);
|
|
82
|
+
this.#entries = Object.entries(this.config.registry);
|
|
83
|
+
this.urlFor = this.#createUrlBuilder();
|
|
84
|
+
this.#applyPlugins();
|
|
85
|
+
this.#client = ky.create(this.#mergeKyConfiguration());
|
|
86
|
+
}
|
|
87
|
+
api;
|
|
88
|
+
urlFor;
|
|
89
|
+
#entries;
|
|
90
|
+
#client;
|
|
91
|
+
/**
|
|
92
|
+
* Merges the default Ky configuration with user-provided config
|
|
93
|
+
*/
|
|
94
|
+
#mergeKyConfiguration() {
|
|
95
|
+
return {
|
|
96
|
+
prefixUrl: this.config.baseUrl,
|
|
97
|
+
...this.config,
|
|
98
|
+
hooks: {
|
|
99
|
+
...this.config.hooks,
|
|
100
|
+
beforeRequest: [
|
|
101
|
+
...this.config.hooks?.beforeRequest || [],
|
|
102
|
+
this.#appendCsrfToken.bind(this)
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Applies registered plugins to the client configuration
|
|
109
|
+
*/
|
|
110
|
+
#applyPlugins() {
|
|
111
|
+
this.config.plugins?.forEach((plugin) => plugin({ options: this.config }));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Creates a URL builder instance for generating URLs based on the route registry
|
|
115
|
+
*/
|
|
116
|
+
#createUrlBuilder() {
|
|
117
|
+
const rootEntries = this.#entries.map(([name, entry]) => ({ name, domain: "root", ...entry }));
|
|
118
|
+
return createUrlBuilder({ root: rootEntries }, buildSearchParams);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Automatically appends CSRF token from cookies to requests
|
|
122
|
+
*/
|
|
123
|
+
#appendCsrfToken(request) {
|
|
124
|
+
const xCsrfToken = globalThis.document?.cookie.split("; ").find((row) => row.startsWith("XSRF-TOKEN="));
|
|
125
|
+
if (!xCsrfToken) return;
|
|
126
|
+
request.headers.set("X-XSRF-TOKEN", decodeURIComponent(xCsrfToken.split("=")[1]));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Checks if a value represents a file for upload
|
|
130
|
+
*/
|
|
131
|
+
#isFile(v) {
|
|
132
|
+
if (isReactNative && isObject(v) && v.uri) return true;
|
|
133
|
+
if (isServer) return v instanceof Blob;
|
|
134
|
+
return v instanceof FileList || v instanceof File;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Checks if an object contains any file uploads
|
|
138
|
+
*/
|
|
139
|
+
#hasFile(obj) {
|
|
140
|
+
if (!obj) return false;
|
|
141
|
+
return Object.values(obj).some((val) => {
|
|
142
|
+
if (Array.isArray(val)) return val.some(this.#isFile);
|
|
143
|
+
return this.#isFile(val);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
#getLowercaseMethod(method) {
|
|
147
|
+
return method.toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
#buildUrl(name, method, args) {
|
|
150
|
+
const lowercaseMethod = this.#getLowercaseMethod(method);
|
|
151
|
+
const usedMethod = lowercaseMethod === "head" || lowercaseMethod === "options" ? "get" : lowercaseMethod;
|
|
152
|
+
return this.urlFor[usedMethod](name, args?.params || {}).url;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Performs the actual HTTP request with proper body formatting
|
|
156
|
+
*/
|
|
157
|
+
async #doFetch(name, method, args) {
|
|
158
|
+
const url = this.#buildUrl(name, method, args);
|
|
159
|
+
let key = "json";
|
|
160
|
+
let body = args.body;
|
|
161
|
+
if (!(body instanceof FormData) && this.#hasFile(body)) {
|
|
162
|
+
body = serialize(body, { indices: true });
|
|
163
|
+
key = "body";
|
|
164
|
+
} else if (body instanceof FormData) {
|
|
165
|
+
key = "body";
|
|
166
|
+
}
|
|
167
|
+
const isGetOrHead = ["GET", "HEAD"].includes(method);
|
|
168
|
+
const { body: _, ...restArgs } = args;
|
|
169
|
+
const requestOptions = {
|
|
170
|
+
searchParams: buildSearchParams(args?.query || {}),
|
|
171
|
+
[key]: !isGetOrHead ? body : void 0,
|
|
172
|
+
...restArgs
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
const res = await this.#client[this.#getLowercaseMethod(method)](
|
|
176
|
+
removeSlash(url),
|
|
177
|
+
requestOptions
|
|
178
|
+
);
|
|
179
|
+
let data;
|
|
180
|
+
const responseType = res.headers.get("Content-Type")?.split(";")[0];
|
|
181
|
+
if (responseType === "application/json") {
|
|
182
|
+
data = await res.json();
|
|
183
|
+
} else if (responseType === "application/octet-stream") {
|
|
184
|
+
data = await res.arrayBuffer();
|
|
185
|
+
} else {
|
|
186
|
+
data = await res.text();
|
|
187
|
+
}
|
|
188
|
+
return data;
|
|
189
|
+
} catch (originalError) {
|
|
190
|
+
if (originalError instanceof HTTPError) {
|
|
191
|
+
const parsedResponse = await parseResponse(originalError.response);
|
|
192
|
+
throw new TuyauHTTPError(originalError, parsedResponse);
|
|
193
|
+
}
|
|
194
|
+
throw new TuyauNetworkError(originalError, { url, method });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Finds an endpoint definition by HTTP method and pattern
|
|
199
|
+
*/
|
|
200
|
+
#byMethodPath(method, pattern) {
|
|
201
|
+
const [name, endpoint] = this.#entries.find(
|
|
202
|
+
([, e]) => e.methods[0] === method && e.pattern === pattern
|
|
203
|
+
) || [null, null];
|
|
204
|
+
if (!name || !endpoint) throw new Error(`No ${method} ${pattern}`);
|
|
205
|
+
return { name, endpoint };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Makes a request to a specific endpoint using method and pattern
|
|
209
|
+
*/
|
|
210
|
+
#request(method, pattern, args) {
|
|
211
|
+
const { name, endpoint } = this.#byMethodPath(method, pattern);
|
|
212
|
+
return this.#doFetch(name, endpoint.methods[0], args);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Makes a GET request to the specified pattern
|
|
216
|
+
*/
|
|
217
|
+
get(pattern, args) {
|
|
218
|
+
return this.#request("GET", pattern, args);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Makes a POST request to the specified pattern
|
|
222
|
+
*/
|
|
223
|
+
post(pattern, args) {
|
|
224
|
+
return this.#request("POST", pattern, args);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Makes a PUT request to the specified pattern
|
|
228
|
+
*/
|
|
229
|
+
put(pattern, args) {
|
|
230
|
+
return this.#request("PUT", pattern, args);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Makes a PATCH request to the specified pattern
|
|
234
|
+
*/
|
|
235
|
+
patch(pattern, args) {
|
|
236
|
+
return this.#request("PATCH", pattern, args);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Makes a DELETE request to the specified pattern
|
|
240
|
+
*/
|
|
241
|
+
delete(pattern, args) {
|
|
242
|
+
return this.#request("DELETE", pattern, args);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Makes a HEAD request to the specified pattern
|
|
246
|
+
*/
|
|
247
|
+
head(pattern, args) {
|
|
248
|
+
return this.#request("HEAD", pattern, args);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Makes a request to a named endpoint
|
|
252
|
+
*/
|
|
253
|
+
request(name, args) {
|
|
254
|
+
const def = this.config.registry[name];
|
|
255
|
+
return this.#doFetch(name, def.methods[0], args);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Gets route information by name including URL and HTTP method
|
|
259
|
+
*/
|
|
260
|
+
getRoute(name, args) {
|
|
261
|
+
const def = this.config.registry[name];
|
|
262
|
+
if (!def) throw new Error(`Route ${String(name)} not found`);
|
|
263
|
+
const url = this.#buildUrl(name, def.methods[0], args);
|
|
264
|
+
return { url, methods: def.methods };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Creates a proxy-based fluent API for accessing endpoints by name
|
|
268
|
+
*/
|
|
269
|
+
makeNamed(segments) {
|
|
270
|
+
const dot = segments.join(".");
|
|
271
|
+
const def = this.config.registry[dot];
|
|
272
|
+
if (def) {
|
|
273
|
+
const fn = (args) => this.#doFetch(dot, def.methods[0], args);
|
|
274
|
+
return new Proxy(fn, {
|
|
275
|
+
get: (_t, prop) => this.makeNamed([...segments, String(prop)]),
|
|
276
|
+
apply: (_t, _this, argArray) => fn(...argArray)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return new Proxy({}, { get: (_t, prop) => this.makeNamed([...segments, String(prop)]) });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function createTuyau(config) {
|
|
283
|
+
return new Tuyau(config);
|
|
284
|
+
}
|
|
285
|
+
export {
|
|
286
|
+
Tuyau,
|
|
287
|
+
TuyauHTTPError,
|
|
288
|
+
TuyauNetworkError,
|
|
289
|
+
createTuyau,
|
|
290
|
+
parseResponse
|
|
291
|
+
};
|