@fragno-dev/core 0.0.1

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.
Files changed (108) hide show
  1. package/.turbo/turbo-build.log +61 -0
  2. package/.turbo/turbo-types$colon$check.log +2 -0
  3. package/dist/api/api.d.ts +2 -0
  4. package/dist/api/api.js +3 -0
  5. package/dist/api-CBDGZiLC.d.ts +278 -0
  6. package/dist/api-CBDGZiLC.d.ts.map +1 -0
  7. package/dist/api-DgHfYjq2.js +54 -0
  8. package/dist/api-DgHfYjq2.js.map +1 -0
  9. package/dist/client/client.d.ts +3 -0
  10. package/dist/client/client.js +6 -0
  11. package/dist/client/client.svelte.d.ts +33 -0
  12. package/dist/client/client.svelte.d.ts.map +1 -0
  13. package/dist/client/client.svelte.js +123 -0
  14. package/dist/client/client.svelte.js.map +1 -0
  15. package/dist/client/react.d.ts +58 -0
  16. package/dist/client/react.d.ts.map +1 -0
  17. package/dist/client/react.js +80 -0
  18. package/dist/client/react.js.map +1 -0
  19. package/dist/client/vanilla.d.ts +61 -0
  20. package/dist/client/vanilla.d.ts.map +1 -0
  21. package/dist/client/vanilla.js +136 -0
  22. package/dist/client/vanilla.js.map +1 -0
  23. package/dist/client/vue.d.ts +39 -0
  24. package/dist/client/vue.d.ts.map +1 -0
  25. package/dist/client/vue.js +108 -0
  26. package/dist/client/vue.js.map +1 -0
  27. package/dist/client-DWjxKDnE.js +703 -0
  28. package/dist/client-DWjxKDnE.js.map +1 -0
  29. package/dist/client-XFdAy-IQ.d.ts +287 -0
  30. package/dist/client-XFdAy-IQ.d.ts.map +1 -0
  31. package/dist/integrations/astro.d.ts +18 -0
  32. package/dist/integrations/astro.d.ts.map +1 -0
  33. package/dist/integrations/astro.js +16 -0
  34. package/dist/integrations/astro.js.map +1 -0
  35. package/dist/integrations/next-js.d.ts +15 -0
  36. package/dist/integrations/next-js.d.ts.map +1 -0
  37. package/dist/integrations/next-js.js +17 -0
  38. package/dist/integrations/next-js.js.map +1 -0
  39. package/dist/integrations/react-ssr.d.ts +19 -0
  40. package/dist/integrations/react-ssr.d.ts.map +1 -0
  41. package/dist/integrations/react-ssr.js +38 -0
  42. package/dist/integrations/react-ssr.js.map +1 -0
  43. package/dist/integrations/svelte-kit.d.ts +21 -0
  44. package/dist/integrations/svelte-kit.d.ts.map +1 -0
  45. package/dist/integrations/svelte-kit.js +18 -0
  46. package/dist/integrations/svelte-kit.js.map +1 -0
  47. package/dist/mod.d.ts +3 -0
  48. package/dist/mod.js +177 -0
  49. package/dist/mod.js.map +1 -0
  50. package/dist/route-Bp6eByhz.js +331 -0
  51. package/dist/route-Bp6eByhz.js.map +1 -0
  52. package/dist/ssr-tJHqcNSw.js +48 -0
  53. package/dist/ssr-tJHqcNSw.js.map +1 -0
  54. package/package.json +127 -0
  55. package/src/api/api.test.ts +140 -0
  56. package/src/api/api.ts +106 -0
  57. package/src/api/error.ts +47 -0
  58. package/src/api/fragment.test.ts +509 -0
  59. package/src/api/fragment.ts +277 -0
  60. package/src/api/internal/path-runtime.test.ts +121 -0
  61. package/src/api/internal/path-type.test.ts +602 -0
  62. package/src/api/internal/path.ts +322 -0
  63. package/src/api/internal/response-stream.ts +118 -0
  64. package/src/api/internal/route.test.ts +56 -0
  65. package/src/api/internal/route.ts +9 -0
  66. package/src/api/request-input-context.test.ts +437 -0
  67. package/src/api/request-input-context.ts +201 -0
  68. package/src/api/request-middleware.test.ts +544 -0
  69. package/src/api/request-middleware.ts +126 -0
  70. package/src/api/request-output-context.test.ts +626 -0
  71. package/src/api/request-output-context.ts +175 -0
  72. package/src/api/route.test.ts +176 -0
  73. package/src/api/route.ts +152 -0
  74. package/src/client/client-builder.test.ts +264 -0
  75. package/src/client/client-error.test.ts +15 -0
  76. package/src/client/client-error.ts +141 -0
  77. package/src/client/client-types.test.ts +493 -0
  78. package/src/client/client.ssr.test.ts +173 -0
  79. package/src/client/client.svelte.test.ts +837 -0
  80. package/src/client/client.svelte.ts +278 -0
  81. package/src/client/client.test.ts +1690 -0
  82. package/src/client/client.ts +1035 -0
  83. package/src/client/component.test.svelte +21 -0
  84. package/src/client/internal/ndjson-streaming.test.ts +457 -0
  85. package/src/client/internal/ndjson-streaming.ts +248 -0
  86. package/src/client/react.test.ts +947 -0
  87. package/src/client/react.ts +241 -0
  88. package/src/client/vanilla.test.ts +867 -0
  89. package/src/client/vanilla.ts +265 -0
  90. package/src/client/vue.test.ts +754 -0
  91. package/src/client/vue.ts +242 -0
  92. package/src/http/http-status.ts +60 -0
  93. package/src/integrations/astro.ts +17 -0
  94. package/src/integrations/next-js.ts +31 -0
  95. package/src/integrations/react-ssr.ts +40 -0
  96. package/src/integrations/svelte-kit.ts +41 -0
  97. package/src/mod.ts +20 -0
  98. package/src/util/async.test.ts +85 -0
  99. package/src/util/async.ts +96 -0
  100. package/src/util/content-type.test.ts +136 -0
  101. package/src/util/content-type.ts +84 -0
  102. package/src/util/nanostores.test.ts +28 -0
  103. package/src/util/nanostores.ts +65 -0
  104. package/src/util/ssr.ts +75 -0
  105. package/src/util/types-util.ts +16 -0
  106. package/tsconfig.json +10 -0
  107. package/tsdown.config.ts +21 -0
  108. package/vitest.config.ts +10 -0
@@ -0,0 +1,322 @@
1
+ // Helper type to split a string by '/'
2
+ type SplitPath<T extends string> = T extends `${infer First}/${infer Rest}`
3
+ ? First extends ""
4
+ ? SplitPath<Rest>
5
+ : [First, ...SplitPath<Rest>]
6
+ : T extends ""
7
+ ? []
8
+ : [T];
9
+
10
+ // Helper type to extract parameter name from a single segment
11
+ type ExtractParam<T extends string> = T extends `:${infer Name}`
12
+ ? Name
13
+ : T extends `**:${infer Name}`
14
+ ? Name
15
+ : T extends "**"
16
+ ? "**"
17
+ : never;
18
+
19
+ // Helper type to extract all parameter names from path segments
20
+ type ExtractParamsFromSegments<T extends readonly string[]> = T extends readonly [
21
+ infer First,
22
+ ...infer Rest,
23
+ ]
24
+ ? First extends string
25
+ ? Rest extends readonly string[]
26
+ ? ExtractParam<First> | ExtractParamsFromSegments<Rest>
27
+ : ExtractParam<First>
28
+ : never
29
+ : never;
30
+
31
+ /**
32
+ * Type helper to extract path parameters from a const string path
33
+ *
34
+ * Supports:
35
+ * - Regular paths: "/path" -> never
36
+ * - Named parameters: "/path/:name" -> "name"
37
+ * - Wildcard paths: "/path/foo/**" -> "**"
38
+ * - Named wildcard paths: "/path/foo/**:name" -> "name"
39
+ * - String (narrows): string -> never
40
+ */
41
+ export type ExtractPathParams<T extends string, ValueType = string> =
42
+ ExtractParamsFromSegments<SplitPath<T>> extends never
43
+ ? Record<string, never>
44
+ : Record<ExtractParamsFromSegments<SplitPath<T>>, ValueType>;
45
+
46
+ /**
47
+ * Same as @see ExtractPathParams, but returns `Record<string, ValueType>` when a string is
48
+ * passed in.
49
+ */
50
+ export type ExtractPathParamsOrWiden<T extends string, ValueType = string> = string extends T
51
+ ? Record<string, ValueType>
52
+ : ExtractPathParams<T, ValueType>;
53
+
54
+ // TODO: MaybeExtractPathParamsOrWiden<string> --> undefined, should that be Record<string, string>?
55
+ /**
56
+ * Same as @see ExtractPathParamsOrWiden, but returns `undefined` when no path parameters in the
57
+ * const.
58
+ */
59
+ export type MaybeExtractPathParamsOrWiden<T extends string, ValueType = string> =
60
+ HasPathParams<T> extends true ? ExtractPathParamsOrWiden<T, ValueType> : undefined;
61
+
62
+ // Alternative version that returns the parameter names as a union type
63
+ export type ExtractPathParamNames<T extends string> = ExtractParamsFromSegments<SplitPath<T>>;
64
+
65
+ // Helper type to extract parameter names as an ordered tuple from path segments
66
+ type ExtractParamNamesAsTuple<T extends readonly string[]> = T extends readonly [
67
+ infer First,
68
+ ...infer Rest,
69
+ ]
70
+ ? First extends string
71
+ ? Rest extends readonly string[]
72
+ ? ExtractParam<First> extends never
73
+ ? ExtractParamNamesAsTuple<Rest>
74
+ : [ExtractParam<First>, ...ExtractParamNamesAsTuple<Rest>]
75
+ : ExtractParam<First> extends never
76
+ ? []
77
+ : [ExtractParam<First>]
78
+ : []
79
+ : [];
80
+
81
+ // Type to convert ExtractPathParamNames result to a string tuple with the same number of elements
82
+ export type ExtractPathParamNamesAsTuple<T extends string> = ExtractParamNamesAsTuple<SplitPath<T>>;
83
+
84
+ // Helper type to create labeled tuple from parameter names
85
+ type CreateLabeledTuple<T extends readonly string[], ElementType = string> = T extends readonly [
86
+ infer First,
87
+ ...infer Rest,
88
+ ]
89
+ ? First extends string
90
+ ? Rest extends readonly string[]
91
+ ? [{ [K in First]: ElementType }[First], ...CreateLabeledTuple<Rest, ElementType>]
92
+ : [{ [K in First]: ElementType }[First]]
93
+ : []
94
+ : [];
95
+
96
+ // Type to convert path parameters to a labeled tuple
97
+ export type ExtractPathParamsAsLabeledTuple<
98
+ T extends string,
99
+ ElementType = string,
100
+ > = CreateLabeledTuple<ExtractParamNamesAsTuple<SplitPath<T>>, ElementType>;
101
+
102
+ // Type to check if a path has parameters
103
+ export type HasPathParams<T extends string> = ExtractPathParamNames<T> extends never ? false : true;
104
+
105
+ /**
106
+ * Creates a query parameters type where the specified keys are hints (optional)
107
+ * and additional string keys are also allowed.
108
+ *
109
+ * This allows for flexible query parameter typing where:
110
+ * - All hinted parameters are optional
111
+ * - Additional parameters beyond the hints are allowed
112
+ * - Values can be of any specified type (defaults to string)
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * type MyQuery = QueryParamsHint<"page" | "limit", string>;
117
+ * // Allows: { page?: string, limit?: string, [key: string]: string }
118
+ *
119
+ * const query1: MyQuery = {}; // Valid - no params required
120
+ * const query2: MyQuery = { page: "1" }; // Valid - hinted param
121
+ * const query3: MyQuery = { page: "1", sort: "asc" }; // Valid - additional param
122
+ * ```
123
+ */
124
+ export type QueryParamsHint<TQueryParameters extends string, ValueType = string> = Partial<
125
+ Record<TQueryParameters, ValueType>
126
+ > &
127
+ Record<string, ValueType>;
128
+
129
+ // Runtime utilities
130
+
131
+ /**
132
+ * Extract parameter names from a path pattern at runtime.
133
+ * Examples:
134
+ * - "/users/:id" => ["id"]
135
+ * - "/files/**" => ["**"]
136
+ * - "/files/**:rest" => ["rest"]
137
+ */
138
+ export function extractPathParams<TPath extends string>(
139
+ pathPattern: TPath,
140
+ ): ExtractPathParamNames<TPath>[] {
141
+ const segments = pathPattern.split("/").filter((s) => s.length > 0);
142
+ const names: string[] = [];
143
+
144
+ for (const segment of segments) {
145
+ if (segment.startsWith(":")) {
146
+ names.push(segment.slice(1));
147
+ continue;
148
+ }
149
+
150
+ if (segment === "**") {
151
+ names.push("**");
152
+ continue;
153
+ }
154
+
155
+ if (segment.startsWith("**:")) {
156
+ names.push(segment.slice(3));
157
+ continue;
158
+ }
159
+ }
160
+
161
+ return names as ExtractPathParamNames<TPath>[];
162
+ }
163
+
164
+ /**
165
+ * Match an actual path against a path pattern and return extracted params.
166
+ *
167
+ * Notes and limitations:
168
+ * - Named segment ":name" captures a single path segment.
169
+ * - Wildcard "**" or "**:name" greedily captures the remainder of the path and
170
+ * should be placed at the end of the pattern.
171
+ * - If the path does not match the pattern, an empty object is returned.
172
+ */
173
+ export function matchPathParams<TPath extends string>(
174
+ pathPattern: TPath,
175
+ actualPath: string,
176
+ ): ExtractPathParams<TPath> {
177
+ const patternSegments = pathPattern.split("/").filter((s) => s.length > 0);
178
+ const actualSegments = actualPath.split("/").filter((s) => s.length > 0);
179
+
180
+ const params: Record<string, string> = {};
181
+
182
+ let i = 0;
183
+ let j = 0;
184
+
185
+ while (i < patternSegments.length && j < actualSegments.length) {
186
+ const patternSegment = patternSegments[i];
187
+ const actualSegment = actualSegments[j];
188
+
189
+ if (patternSegment.startsWith(":")) {
190
+ const name = patternSegment.slice(1);
191
+ params[name] = decodeURIComponent(actualSegment);
192
+ i += 1;
193
+ j += 1;
194
+ continue;
195
+ }
196
+
197
+ if (patternSegment === "**") {
198
+ const remainder = actualSegments.slice(j).join("/");
199
+ params["**"] = remainder ? decodeURIComponent(remainder) : "";
200
+ // Wildcard consumes the rest; pattern should end here
201
+ i = patternSegments.length;
202
+ j = actualSegments.length;
203
+ break;
204
+ }
205
+
206
+ if (patternSegment.startsWith("**:")) {
207
+ const name = patternSegment.slice(3);
208
+ const remainder = actualSegments.slice(j).join("/");
209
+ params[name] = remainder ? decodeURIComponent(remainder) : "";
210
+ // Wildcard consumes the rest; pattern should end here
211
+ i = patternSegments.length;
212
+ j = actualSegments.length;
213
+ break;
214
+ }
215
+
216
+ // Literal segment must match exactly
217
+ if (patternSegment === actualSegment) {
218
+ i += 1;
219
+ j += 1;
220
+ continue;
221
+ }
222
+
223
+ // Mismatch
224
+ return {} as ExtractPathParams<TPath>;
225
+ }
226
+
227
+ // If there are remaining pattern segments
228
+ while (i < patternSegments.length) {
229
+ const remaining = patternSegments[i];
230
+ if (remaining === "**") {
231
+ params["**"] = "";
232
+ i += 1;
233
+ continue;
234
+ }
235
+ if (remaining.startsWith(":")) {
236
+ const name = remaining.slice(1);
237
+ params[name] = "";
238
+ i += 1;
239
+ continue;
240
+ }
241
+ if (remaining.startsWith("**:")) {
242
+ const name = remaining.slice(3);
243
+ params[name] = "";
244
+ i += 1;
245
+ continue;
246
+ }
247
+ // Non-parameter remaining segment without corresponding actual segment → mismatch
248
+ return {} as ExtractPathParams<TPath>;
249
+ }
250
+
251
+ // If there are remaining actual segments without pattern to match → mismatch
252
+ if (j < actualSegments.length) {
253
+ return {} as ExtractPathParams<TPath>;
254
+ }
255
+
256
+ return params as ExtractPathParams<TPath>;
257
+ }
258
+
259
+ /**
260
+ * Build a concrete path by replacing placeholders in a path pattern with values.
261
+ *
262
+ * Supports the same placeholder syntax as the matcher:
263
+ * - Named parameter ":name" is URL-encoded as a single segment
264
+ * - Anonymous wildcard "**" inserts the remainder as-is (slashes preserved)
265
+ * - Named wildcard "**:name" inserts the remainder from the named key
266
+ *
267
+ * Examples:
268
+ * - buildPath("/users/:id", { id: "123" }) => "/users/123"
269
+ * - buildPath("/files/**", { "**": "a/b" }) => "/files/a/b"
270
+ * - buildPath("/files/**:rest", { rest: "a/b" }) => "/files/a/b"
271
+ */
272
+ export function buildPath<TPath extends string>(
273
+ pathPattern: TPath,
274
+ params: ExtractPathParams<TPath>,
275
+ ): string {
276
+ const patternSegments = pathPattern.split("/");
277
+
278
+ const builtSegments: string[] = [];
279
+
280
+ for (const segment of patternSegments) {
281
+ if (segment.length === 0) {
282
+ // Preserve leading/trailing/duplicate slashes
283
+ builtSegments.push("");
284
+ continue;
285
+ }
286
+
287
+ if (segment.startsWith(":")) {
288
+ const name = segment.slice(1);
289
+ const value = (params as Record<string, string | undefined>)[name];
290
+ if (value === undefined) {
291
+ throw new Error(`Missing value for path parameter :${name}`);
292
+ }
293
+ builtSegments.push(encodeURIComponent(value));
294
+ continue;
295
+ }
296
+
297
+ if (segment === "**") {
298
+ const value = (params as Record<string, string | undefined>)["**"];
299
+ if (value === undefined) {
300
+ throw new Error("Missing value for path wildcard **");
301
+ }
302
+ builtSegments.push(value);
303
+ continue;
304
+ }
305
+
306
+ if (segment.startsWith("**:")) {
307
+ const name = segment.slice(3);
308
+ const value = (params as Record<string, string | undefined>)[name];
309
+ if (value === undefined) {
310
+ throw new Error(`Missing value for path wildcard **:${name}`);
311
+ }
312
+ builtSegments.push(value);
313
+ continue;
314
+ }
315
+
316
+ // Literal segment
317
+ builtSegments.push(segment);
318
+ }
319
+
320
+ // Join with '/'. Empty segments preserve leading/trailing slashes
321
+ return builtSegments.join("/");
322
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @module
3
+ * Stream utility.
4
+ *
5
+ * Modified from honojs/hono
6
+ * Original source: https://github.com/honojs/hono/blob/0e3db674ad3f40be215a55a18062dd8e387ce525/src/utils/stream.ts
7
+ * License: MIT
8
+ * Date obtained: August 28 2025
9
+ * Copyright (c) 2021-present Yusuke Wada and Hono contributors
10
+ */
11
+
12
+ type Error<Message extends string> = { __errorMessage: Message };
13
+
14
+ export class ResponseStream<TArray> {
15
+ #writer: WritableStreamDefaultWriter<Uint8Array>;
16
+ #encoder: TextEncoder;
17
+ #abortSubscribers: (() => void | Promise<void>)[] = [];
18
+ #responseReadable: ReadableStream;
19
+
20
+ #aborted: boolean = false;
21
+ #closed: boolean = false;
22
+
23
+ /**
24
+ * Whether the stream has been aborted.
25
+ */
26
+ get aborted(): boolean {
27
+ return this.#aborted;
28
+ }
29
+
30
+ /**
31
+ * Whether the stream has been closed normally.
32
+ */
33
+ get closed(): boolean {
34
+ return this.#closed;
35
+ }
36
+
37
+ /**
38
+ * The readable stream that the response is piped to.
39
+ */
40
+ get responseReadable(): ReadableStream {
41
+ return this.#responseReadable;
42
+ }
43
+
44
+ constructor(writable: WritableStream, readable: ReadableStream) {
45
+ this.#writer = writable.getWriter();
46
+ this.#encoder = new TextEncoder();
47
+ const reader = readable.getReader();
48
+
49
+ // in case the user disconnects, let the reader know to cancel
50
+ // this in-turn results in responseReadable being closed
51
+ // and writeSSE method no longer blocks indefinitely
52
+ this.#abortSubscribers.push(async () => {
53
+ await reader.cancel();
54
+ });
55
+
56
+ this.#responseReadable = new ReadableStream({
57
+ async pull(controller) {
58
+ const { done, value } = await reader.read();
59
+ if (done) {
60
+ controller.close();
61
+ } else {
62
+ controller.enqueue(value);
63
+ }
64
+ },
65
+ cancel: () => {
66
+ this.abort();
67
+ },
68
+ });
69
+ }
70
+
71
+ async writeRaw(input: Uint8Array | string): Promise<void> {
72
+ try {
73
+ if (typeof input === "string") {
74
+ input = this.#encoder.encode(input);
75
+ }
76
+ await this.#writer.write(input);
77
+ } catch {
78
+ // Do nothing.
79
+ }
80
+ }
81
+
82
+ write(
83
+ input: TArray extends (infer U)[]
84
+ ? U
85
+ : Error<"To use a streaming response, outputSchema must be an array.">,
86
+ ): Promise<void> {
87
+ return this.writeRaw(JSON.stringify(input) + "\n");
88
+ }
89
+
90
+ sleep(ms: number): Promise<unknown> {
91
+ return new Promise((res) => setTimeout(res, ms));
92
+ }
93
+
94
+ async close() {
95
+ try {
96
+ await this.#writer.close();
97
+ } catch {
98
+ // Do nothing. If you want to handle errors, create a stream by yourself.
99
+ } finally {
100
+ this.#closed = true;
101
+ }
102
+ }
103
+
104
+ onAbort(listener: () => void | Promise<void>) {
105
+ this.#abortSubscribers.push(listener);
106
+ }
107
+
108
+ /**
109
+ * Abort the stream.
110
+ * You can call this method when stream is aborted by external event.
111
+ */
112
+ abort() {
113
+ if (!this.aborted) {
114
+ this.#aborted = true;
115
+ this.#abortSubscribers.forEach((subscriber) => subscriber());
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,56 @@
1
+ import { test, expect } from "vitest";
2
+ import { getMountRoute } from "./route";
3
+
4
+ test("getMountRoute - default mount route", () => {
5
+ expect(getMountRoute({ name: "test" })).toBe("/api/test");
6
+ });
7
+
8
+ test("getMountRoute - custom mount route without trailing slash", () => {
9
+ expect(getMountRoute({ name: "test", mountRoute: "/custom/path" })).toBe("/custom/path");
10
+ });
11
+
12
+ test("getMountRoute - custom mount route with trailing slash", () => {
13
+ expect(getMountRoute({ name: "test", mountRoute: "/custom/path/" })).toBe("/custom/path");
14
+ });
15
+
16
+ test("getMountRoute - multiple trailing slashes", () => {
17
+ expect(getMountRoute({ name: "test", mountRoute: "/custom/path///" })).toBe("/custom/path//");
18
+ });
19
+
20
+ test("getMountRoute - root path", () => {
21
+ expect(getMountRoute({ name: "test", mountRoute: "/" })).toBe("");
22
+ });
23
+
24
+ test("getMountRoute - empty name", () => {
25
+ expect(getMountRoute({ name: "" })).toBe("/api");
26
+ });
27
+
28
+ test("getMountRoute - name with special characters", () => {
29
+ expect(getMountRoute({ name: "test-api_v1" })).toBe("/api/test-api_v1");
30
+ });
31
+
32
+ test("getMountRoute - name with spaces", () => {
33
+ expect(getMountRoute({ name: "test api" })).toBe("/api/test api");
34
+ });
35
+
36
+ test("getMountRoute - custom mount route with query parameters", () => {
37
+ expect(getMountRoute({ name: "test", mountRoute: "/api/v1?version=latest" })).toBe(
38
+ "/api/v1?version=latest",
39
+ );
40
+ });
41
+
42
+ test("getMountRoute - custom mount route with fragment", () => {
43
+ expect(getMountRoute({ name: "test", mountRoute: "/api/v1#section" })).toBe("/api/v1#section");
44
+ });
45
+
46
+ test("getMountRoute - deeply nested path", () => {
47
+ expect(
48
+ getMountRoute({ name: "deeply-nested", mountRoute: "/api/v1/users/profile/settings" }),
49
+ ).toBe("/api/v1/users/profile/settings");
50
+ });
51
+
52
+ test("getMountRoute - deeply nested path with trailing slash", () => {
53
+ expect(
54
+ getMountRoute({ name: "deeply-nested", mountRoute: "/api/v1/users/profile/settings/" }),
55
+ ).toBe("/api/v1/users/profile/settings");
56
+ });
@@ -0,0 +1,9 @@
1
+ export function getMountRoute(opts: { mountRoute?: string; name: string }) {
2
+ const mountRoute = opts.mountRoute ?? `/api/${opts.name}`;
3
+
4
+ if (mountRoute.endsWith("/")) {
5
+ return mountRoute.slice(0, -1);
6
+ }
7
+
8
+ return mountRoute;
9
+ }