@carno.js/core 1.0.2 → 1.0.4
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/LICENSE +21 -674
- package/README.md +188 -188
- package/dist/bun/index.js +7 -20
- package/dist/bun/index.js.map +28 -28
- package/dist/context/Context.js +2 -1
- package/dist/context/Context.mjs +2 -1
- package/dist/utils/parseQuery.js +84 -0
- package/dist/utils/parseQuery.mjs +63 -0
- package/package.json +3 -2
- package/src/Carno.ts +605 -605
- package/src/DefaultRoutes.ts +34 -34
- package/src/cache/CacheDriver.ts +50 -50
- package/src/cache/CacheService.ts +139 -139
- package/src/cache/MemoryDriver.ts +104 -104
- package/src/cache/RedisDriver.ts +116 -116
- package/src/compiler/JITCompiler.ts +167 -167
- package/src/container/Container.ts +168 -168
- package/src/context/Context.ts +130 -128
- package/src/cors/CorsHandler.ts +145 -145
- package/src/decorators/Controller.ts +63 -63
- package/src/decorators/Inject.ts +16 -16
- package/src/decorators/Middleware.ts +22 -22
- package/src/decorators/Service.ts +18 -18
- package/src/decorators/methods.ts +58 -58
- package/src/decorators/params.ts +47 -47
- package/src/events/Lifecycle.ts +97 -97
- package/src/exceptions/HttpException.ts +99 -99
- package/src/index.ts +92 -92
- package/src/metadata.ts +46 -46
- package/src/middleware/CarnoMiddleware.ts +14 -14
- package/src/router/RadixRouter.ts +225 -225
- package/src/testing/TestHarness.ts +177 -177
- package/src/utils/Metadata.ts +43 -43
- package/src/utils/parseQuery.ts +161 -0
- package/src/validation/ValibotAdapter.ts +95 -95
- package/src/validation/ValidatorAdapter.ts +69 -69
- package/src/validation/ZodAdapter.ts +102 -102
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
import type { Server } from 'bun';
|
|
2
|
-
import { Carno, type CarnoConfig } from '../Carno';
|
|
3
|
-
import { Container, type Token } from '../container/Container';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Test configuration options.
|
|
7
|
-
*/
|
|
8
|
-
export interface TestOptions {
|
|
9
|
-
config?: CarnoConfig;
|
|
10
|
-
listen?: boolean | number;
|
|
11
|
-
port?: number;
|
|
12
|
-
controllers?: (new (...args: any[]) => any)[];
|
|
13
|
-
services?: (Token | any)[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Test harness - provides utilities for testing Carno applications.
|
|
18
|
-
*/
|
|
19
|
-
export interface TestHarness {
|
|
20
|
-
/** The Carno app instance */
|
|
21
|
-
app: Carno;
|
|
22
|
-
|
|
23
|
-
/** The internal DI container */
|
|
24
|
-
container: Container;
|
|
25
|
-
|
|
26
|
-
/** The HTTP server (if listening) */
|
|
27
|
-
server?: Server<any>;
|
|
28
|
-
|
|
29
|
-
/** The port the server is running on */
|
|
30
|
-
port?: number;
|
|
31
|
-
|
|
32
|
-
/** Resolve a service from the container */
|
|
33
|
-
resolve<T>(token: Token<T>): T;
|
|
34
|
-
|
|
35
|
-
/** Make an HTTP request to the app */
|
|
36
|
-
request(path: string, init?: RequestInit): Promise<Response>;
|
|
37
|
-
|
|
38
|
-
/** Make a GET request */
|
|
39
|
-
get(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
40
|
-
|
|
41
|
-
/** Make a POST request */
|
|
42
|
-
post(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
43
|
-
|
|
44
|
-
/** Make a PUT request */
|
|
45
|
-
put(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
46
|
-
|
|
47
|
-
/** Make a DELETE request */
|
|
48
|
-
delete(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
49
|
-
|
|
50
|
-
/** Close the test harness and cleanup */
|
|
51
|
-
close(): Promise<void>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Create a test harness for Turbo applications.
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* const harness = await createTestHarness({
|
|
60
|
-
* controllers: [UserController],
|
|
61
|
-
* services: [UserService],
|
|
62
|
-
* listen: true
|
|
63
|
-
* });
|
|
64
|
-
*
|
|
65
|
-
* const response = await harness.get('/users');
|
|
66
|
-
* expect(response.status).toBe(200);
|
|
67
|
-
*
|
|
68
|
-
* await harness.close();
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export async function createTestHarness(options: TestOptions = {}): Promise<TestHarness> {
|
|
72
|
-
const config: CarnoConfig = {
|
|
73
|
-
...options.config,
|
|
74
|
-
disableStartupLog: true
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const app = new Carno(config);
|
|
78
|
-
|
|
79
|
-
// Register controllers
|
|
80
|
-
if (options.controllers) {
|
|
81
|
-
app.controllers(options.controllers);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Register services
|
|
85
|
-
if (options.services) {
|
|
86
|
-
app.services(options.services);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const port = resolvePort(options);
|
|
90
|
-
let server: Server<any> | undefined;
|
|
91
|
-
|
|
92
|
-
if (shouldListen(options.listen)) {
|
|
93
|
-
app.listen(port);
|
|
94
|
-
server = (app as any).server;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const actualPort = server?.port ?? port;
|
|
98
|
-
const container = (app as any).container as Container;
|
|
99
|
-
|
|
100
|
-
// Pre-bind methods for performance
|
|
101
|
-
const baseUrl = `http://127.0.0.1:${actualPort}`;
|
|
102
|
-
|
|
103
|
-
const request = async (path: string, init?: RequestInit): Promise<Response> => {
|
|
104
|
-
if (!server) {
|
|
105
|
-
throw new Error('Server not running. Set listen: true in options.');
|
|
106
|
-
}
|
|
107
|
-
const url = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
108
|
-
return fetch(url, init);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
app,
|
|
113
|
-
container,
|
|
114
|
-
server,
|
|
115
|
-
port: actualPort,
|
|
116
|
-
|
|
117
|
-
resolve: <T>(token: Token<T>): T => container.get(token),
|
|
118
|
-
|
|
119
|
-
request,
|
|
120
|
-
|
|
121
|
-
get: (path, init) => request(path, { ...init, method: 'GET' }),
|
|
122
|
-
|
|
123
|
-
post: (path, body, init) => request(path, {
|
|
124
|
-
...init,
|
|
125
|
-
method: 'POST',
|
|
126
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
127
|
-
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
128
|
-
}),
|
|
129
|
-
|
|
130
|
-
put: (path, body, init) => request(path, {
|
|
131
|
-
...init,
|
|
132
|
-
method: 'PUT',
|
|
133
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
134
|
-
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
135
|
-
}),
|
|
136
|
-
|
|
137
|
-
delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
|
|
138
|
-
|
|
139
|
-
close: async () => {
|
|
140
|
-
app.stop();
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Run a test routine with automatic harness cleanup.
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* ```typescript
|
|
150
|
-
* await withTestApp(async (harness) => {
|
|
151
|
-
* const response = await harness.get('/health');
|
|
152
|
-
* expect(response.status).toBe(200);
|
|
153
|
-
* }, { controllers: [HealthController], listen: true });
|
|
154
|
-
* ```
|
|
155
|
-
*/
|
|
156
|
-
export async function withTestApp(
|
|
157
|
-
routine: (harness: TestHarness) => Promise<void>,
|
|
158
|
-
options: TestOptions = {}
|
|
159
|
-
): Promise<void> {
|
|
160
|
-
const harness = await createTestHarness(options);
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
await routine(harness);
|
|
164
|
-
} finally {
|
|
165
|
-
await harness.close();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function shouldListen(value: TestOptions['listen']): boolean {
|
|
170
|
-
return typeof value === 'number' || Boolean(value);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resolvePort(options: TestOptions): number {
|
|
174
|
-
if (typeof options.listen === 'number') return options.listen;
|
|
175
|
-
if (typeof options.port === 'number') return options.port;
|
|
176
|
-
return 0; // Random port
|
|
177
|
-
}
|
|
1
|
+
import type { Server } from 'bun';
|
|
2
|
+
import { Carno, type CarnoConfig } from '../Carno';
|
|
3
|
+
import { Container, type Token } from '../container/Container';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test configuration options.
|
|
7
|
+
*/
|
|
8
|
+
export interface TestOptions {
|
|
9
|
+
config?: CarnoConfig;
|
|
10
|
+
listen?: boolean | number;
|
|
11
|
+
port?: number;
|
|
12
|
+
controllers?: (new (...args: any[]) => any)[];
|
|
13
|
+
services?: (Token | any)[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Test harness - provides utilities for testing Carno applications.
|
|
18
|
+
*/
|
|
19
|
+
export interface TestHarness {
|
|
20
|
+
/** The Carno app instance */
|
|
21
|
+
app: Carno;
|
|
22
|
+
|
|
23
|
+
/** The internal DI container */
|
|
24
|
+
container: Container;
|
|
25
|
+
|
|
26
|
+
/** The HTTP server (if listening) */
|
|
27
|
+
server?: Server<any>;
|
|
28
|
+
|
|
29
|
+
/** The port the server is running on */
|
|
30
|
+
port?: number;
|
|
31
|
+
|
|
32
|
+
/** Resolve a service from the container */
|
|
33
|
+
resolve<T>(token: Token<T>): T;
|
|
34
|
+
|
|
35
|
+
/** Make an HTTP request to the app */
|
|
36
|
+
request(path: string, init?: RequestInit): Promise<Response>;
|
|
37
|
+
|
|
38
|
+
/** Make a GET request */
|
|
39
|
+
get(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
40
|
+
|
|
41
|
+
/** Make a POST request */
|
|
42
|
+
post(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
43
|
+
|
|
44
|
+
/** Make a PUT request */
|
|
45
|
+
put(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
46
|
+
|
|
47
|
+
/** Make a DELETE request */
|
|
48
|
+
delete(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
49
|
+
|
|
50
|
+
/** Close the test harness and cleanup */
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a test harness for Turbo applications.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const harness = await createTestHarness({
|
|
60
|
+
* controllers: [UserController],
|
|
61
|
+
* services: [UserService],
|
|
62
|
+
* listen: true
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* const response = await harness.get('/users');
|
|
66
|
+
* expect(response.status).toBe(200);
|
|
67
|
+
*
|
|
68
|
+
* await harness.close();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export async function createTestHarness(options: TestOptions = {}): Promise<TestHarness> {
|
|
72
|
+
const config: CarnoConfig = {
|
|
73
|
+
...options.config,
|
|
74
|
+
disableStartupLog: true
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const app = new Carno(config);
|
|
78
|
+
|
|
79
|
+
// Register controllers
|
|
80
|
+
if (options.controllers) {
|
|
81
|
+
app.controllers(options.controllers);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Register services
|
|
85
|
+
if (options.services) {
|
|
86
|
+
app.services(options.services);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const port = resolvePort(options);
|
|
90
|
+
let server: Server<any> | undefined;
|
|
91
|
+
|
|
92
|
+
if (shouldListen(options.listen)) {
|
|
93
|
+
app.listen(port);
|
|
94
|
+
server = (app as any).server;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const actualPort = server?.port ?? port;
|
|
98
|
+
const container = (app as any).container as Container;
|
|
99
|
+
|
|
100
|
+
// Pre-bind methods for performance
|
|
101
|
+
const baseUrl = `http://127.0.0.1:${actualPort}`;
|
|
102
|
+
|
|
103
|
+
const request = async (path: string, init?: RequestInit): Promise<Response> => {
|
|
104
|
+
if (!server) {
|
|
105
|
+
throw new Error('Server not running. Set listen: true in options.');
|
|
106
|
+
}
|
|
107
|
+
const url = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
108
|
+
return fetch(url, init);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
app,
|
|
113
|
+
container,
|
|
114
|
+
server,
|
|
115
|
+
port: actualPort,
|
|
116
|
+
|
|
117
|
+
resolve: <T>(token: Token<T>): T => container.get(token),
|
|
118
|
+
|
|
119
|
+
request,
|
|
120
|
+
|
|
121
|
+
get: (path, init) => request(path, { ...init, method: 'GET' }),
|
|
122
|
+
|
|
123
|
+
post: (path, body, init) => request(path, {
|
|
124
|
+
...init,
|
|
125
|
+
method: 'POST',
|
|
126
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
127
|
+
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
put: (path, body, init) => request(path, {
|
|
131
|
+
...init,
|
|
132
|
+
method: 'PUT',
|
|
133
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
134
|
+
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
135
|
+
}),
|
|
136
|
+
|
|
137
|
+
delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
|
|
138
|
+
|
|
139
|
+
close: async () => {
|
|
140
|
+
app.stop();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a test routine with automatic harness cleanup.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* await withTestApp(async (harness) => {
|
|
151
|
+
* const response = await harness.get('/health');
|
|
152
|
+
* expect(response.status).toBe(200);
|
|
153
|
+
* }, { controllers: [HealthController], listen: true });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export async function withTestApp(
|
|
157
|
+
routine: (harness: TestHarness) => Promise<void>,
|
|
158
|
+
options: TestOptions = {}
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const harness = await createTestHarness(options);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await routine(harness);
|
|
164
|
+
} finally {
|
|
165
|
+
await harness.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function shouldListen(value: TestOptions['listen']): boolean {
|
|
170
|
+
return typeof value === 'number' || Boolean(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolvePort(options: TestOptions): number {
|
|
174
|
+
if (typeof options.listen === 'number') return options.listen;
|
|
175
|
+
if (typeof options.port === 'number') return options.port;
|
|
176
|
+
return 0; // Random port
|
|
177
|
+
}
|
package/src/utils/Metadata.ts
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility class for handling metadata operations.
|
|
3
|
-
* Wraps Reflect.getMetadata and Reflect.defineMetadata.
|
|
4
|
-
*/
|
|
5
|
-
export class Metadata {
|
|
6
|
-
static get<T = any>(key: string | symbol, target: any): T | undefined {
|
|
7
|
-
return Reflect.getMetadata(key, target);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
static set(key: string | symbol, value: any, target: any): void {
|
|
11
|
-
Reflect.defineMetadata(key, value, target);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
static has(key: string | symbol, target: any): boolean {
|
|
15
|
-
return Reflect.hasMetadata(key, target);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static delete(key: string | symbol, target: any): boolean {
|
|
19
|
-
return Reflect.deleteMetadata(key, target);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static keys(target: any): (string | symbol)[] {
|
|
23
|
-
return Reflect.getMetadataKeys(target);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
static getType(target: any, propertyKey: string | symbol): any {
|
|
27
|
-
return Reflect.getMetadata('design:type', target, propertyKey);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Type guard for checking if value is an object.
|
|
33
|
-
*/
|
|
34
|
-
export function isObject(value: unknown): value is Record<string, any> {
|
|
35
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Type guard for checking if value is a string.
|
|
40
|
-
*/
|
|
41
|
-
export function isString(value: unknown): value is string {
|
|
42
|
-
return typeof value === 'string';
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for handling metadata operations.
|
|
3
|
+
* Wraps Reflect.getMetadata and Reflect.defineMetadata.
|
|
4
|
+
*/
|
|
5
|
+
export class Metadata {
|
|
6
|
+
static get<T = any>(key: string | symbol, target: any): T | undefined {
|
|
7
|
+
return Reflect.getMetadata(key, target);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static set(key: string | symbol, value: any, target: any): void {
|
|
11
|
+
Reflect.defineMetadata(key, value, target);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static has(key: string | symbol, target: any): boolean {
|
|
15
|
+
return Reflect.hasMetadata(key, target);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static delete(key: string | symbol, target: any): boolean {
|
|
19
|
+
return Reflect.deleteMetadata(key, target);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static keys(target: any): (string | symbol)[] {
|
|
23
|
+
return Reflect.getMetadataKeys(target);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static getType(target: any, propertyKey: string | symbol): any {
|
|
27
|
+
return Reflect.getMetadata('design:type', target, propertyKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Type guard for checking if value is an object.
|
|
33
|
+
*/
|
|
34
|
+
export function isObject(value: unknown): value is Record<string, any> {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type guard for checking if value is a string.
|
|
40
|
+
*/
|
|
41
|
+
export function isString(value: unknown): value is string {
|
|
42
|
+
return typeof value === 'string';
|
|
43
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-performance query string parser.
|
|
3
|
+
*
|
|
4
|
+
* Based on Elysia's approach - uses manual string parsing with charCodeAt()
|
|
5
|
+
* instead of new URL() for significant performance gains.
|
|
6
|
+
*
|
|
7
|
+
* Benchmark: ~10x faster than new URL().searchParams
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Bit flags for tracking decode requirements
|
|
11
|
+
const KEY_HAS_PLUS = 1;
|
|
12
|
+
const KEY_NEEDS_DECODE = 2;
|
|
13
|
+
const VALUE_HAS_PLUS = 4;
|
|
14
|
+
const VALUE_NEEDS_DECODE = 8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse query string from a full URL.
|
|
18
|
+
* Extracts the query portion and parses key-value pairs.
|
|
19
|
+
*
|
|
20
|
+
* @param url Full URL string (e.g., "http://localhost/path?foo=bar&baz=123")
|
|
21
|
+
* @returns Record<string, string> - parsed query parameters
|
|
22
|
+
*/
|
|
23
|
+
export function parseQueryFromURL(url: string): Record<string, string> {
|
|
24
|
+
// Find the start of query string
|
|
25
|
+
const queryStart = url.indexOf('?');
|
|
26
|
+
|
|
27
|
+
if (queryStart === -1) {
|
|
28
|
+
return Object.create(null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find the end of query string (before hash if present)
|
|
32
|
+
let queryEnd = url.indexOf('#', queryStart);
|
|
33
|
+
|
|
34
|
+
if (queryEnd === -1) {
|
|
35
|
+
queryEnd = url.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parseQuery(url, queryStart + 1, queryEnd);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse query string directly.
|
|
43
|
+
*
|
|
44
|
+
* @param input Query string without leading '?' (e.g., "foo=bar&baz=123")
|
|
45
|
+
* @returns Record<string, string> - parsed query parameters
|
|
46
|
+
*/
|
|
47
|
+
export function parseQueryString(input: string): Record<string, string> {
|
|
48
|
+
return parseQuery(input, 0, input.length);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Internal parser - parses query string from startIndex to endIndex.
|
|
53
|
+
*/
|
|
54
|
+
function parseQuery(
|
|
55
|
+
input: string,
|
|
56
|
+
startIndex: number,
|
|
57
|
+
endIndex: number
|
|
58
|
+
): Record<string, string> {
|
|
59
|
+
const result: Record<string, string> = Object.create(null);
|
|
60
|
+
|
|
61
|
+
let flags = 0;
|
|
62
|
+
let startingIndex = startIndex - 1;
|
|
63
|
+
let equalityIndex = startingIndex;
|
|
64
|
+
|
|
65
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
66
|
+
switch (input.charCodeAt(i)) {
|
|
67
|
+
// '&' - separator between key-value pairs
|
|
68
|
+
case 38:
|
|
69
|
+
processKeyValuePair(i);
|
|
70
|
+
startingIndex = i;
|
|
71
|
+
equalityIndex = i;
|
|
72
|
+
flags = 0;
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
// '=' - separator between key and value
|
|
76
|
+
case 61:
|
|
77
|
+
if (equalityIndex <= startingIndex) {
|
|
78
|
+
equalityIndex = i;
|
|
79
|
+
} else {
|
|
80
|
+
// Multiple '=' means value needs decode
|
|
81
|
+
flags |= VALUE_NEEDS_DECODE;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
// '+' - space encoding
|
|
86
|
+
case 43:
|
|
87
|
+
if (equalityIndex > startingIndex) {
|
|
88
|
+
flags |= VALUE_HAS_PLUS;
|
|
89
|
+
} else {
|
|
90
|
+
flags |= KEY_HAS_PLUS;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
// '%' - URL encoding
|
|
95
|
+
case 37:
|
|
96
|
+
if (equalityIndex > startingIndex) {
|
|
97
|
+
flags |= VALUE_NEEDS_DECODE;
|
|
98
|
+
} else {
|
|
99
|
+
flags |= KEY_NEEDS_DECODE;
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Process the last pair
|
|
106
|
+
if (startingIndex < endIndex) {
|
|
107
|
+
processKeyValuePair(endIndex);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
|
|
112
|
+
function processKeyValuePair(pairEndIndex: number) {
|
|
113
|
+
const hasBothKeyValuePair = equalityIndex > startingIndex;
|
|
114
|
+
const effectiveEqualityIndex = hasBothKeyValuePair
|
|
115
|
+
? equalityIndex
|
|
116
|
+
: pairEndIndex;
|
|
117
|
+
|
|
118
|
+
const keySlice = input.slice(startingIndex + 1, effectiveEqualityIndex);
|
|
119
|
+
|
|
120
|
+
// Skip empty keys
|
|
121
|
+
if (!hasBothKeyValuePair && keySlice.length === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let finalKey = keySlice;
|
|
126
|
+
|
|
127
|
+
if (flags & KEY_HAS_PLUS) {
|
|
128
|
+
finalKey = finalKey.replace(/\+/g, ' ');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (flags & KEY_NEEDS_DECODE) {
|
|
132
|
+
try {
|
|
133
|
+
finalKey = decodeURIComponent(finalKey);
|
|
134
|
+
} catch {
|
|
135
|
+
// Keep original if decode fails
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let finalValue = '';
|
|
140
|
+
|
|
141
|
+
if (hasBothKeyValuePair) {
|
|
142
|
+
let valueSlice = input.slice(equalityIndex + 1, pairEndIndex);
|
|
143
|
+
|
|
144
|
+
if (flags & VALUE_HAS_PLUS) {
|
|
145
|
+
valueSlice = valueSlice.replace(/\+/g, ' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (flags & VALUE_NEEDS_DECODE) {
|
|
149
|
+
try {
|
|
150
|
+
finalValue = decodeURIComponent(valueSlice);
|
|
151
|
+
} catch {
|
|
152
|
+
finalValue = valueSlice;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
finalValue = valueSlice;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
result[finalKey] = finalValue;
|
|
160
|
+
}
|
|
161
|
+
}
|