@ailura/nestjs-hono-adapter 1.0.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/LICENSE +21 -0
- package/README.md +411 -0
- package/dist/body.d.ts +35 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +180 -0
- package/dist/body.js.map +1 -0
- package/dist/bridge.d.ts +64 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +168 -0
- package/dist/bridge.js.map +1 -0
- package/dist/closing.d.ts +13 -0
- package/dist/closing.d.ts.map +1 -0
- package/dist/closing.js +30 -0
- package/dist/closing.js.map +1 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/cors-middleware.d.ts +64 -0
- package/dist/cors-middleware.d.ts.map +1 -0
- package/dist/cors-middleware.js +211 -0
- package/dist/cors-middleware.js.map +1 -0
- package/dist/handler-bridge.d.ts +51 -0
- package/dist/handler-bridge.d.ts.map +1 -0
- package/dist/handler-bridge.js +122 -0
- package/dist/handler-bridge.js.map +1 -0
- package/dist/hono-lifecycle.d.ts +90 -0
- package/dist/hono-lifecycle.d.ts.map +1 -0
- package/dist/hono-lifecycle.js +169 -0
- package/dist/hono-lifecycle.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/path.d.ts +3 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +144 -0
- package/dist/path.js.map +1 -0
- package/dist/query.d.ts +19 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +238 -0
- package/dist/query.js.map +1 -0
- package/dist/response-helpers.d.ts +16 -0
- package/dist/response-helpers.d.ts.map +1 -0
- package/dist/response-helpers.js +45 -0
- package/dist/response-helpers.js.map +1 -0
- package/dist/response-writer.d.ts +28 -0
- package/dist/response-writer.d.ts.map +1 -0
- package/dist/response-writer.js +52 -0
- package/dist/response-writer.js.map +1 -0
- package/dist/route-adapter.d.ts +53 -0
- package/dist/route-adapter.d.ts.map +1 -0
- package/dist/route-adapter.js +141 -0
- package/dist/route-adapter.js.map +1 -0
- package/dist/server-adapter.d.ts +106 -0
- package/dist/server-adapter.d.ts.map +1 -0
- package/dist/server-adapter.js +153 -0
- package/dist/server-adapter.js.map +1 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +211 -0
- package/dist/sse.js.map +1 -0
- package/dist/static-assets.d.ts +39 -0
- package/dist/static-assets.d.ts.map +1 -0
- package/dist/static-assets.js +155 -0
- package/dist/static-assets.js.map +1 -0
- package/dist/version-filter.d.ts +24 -0
- package/dist/version-filter.d.ts.map +1 -0
- package/dist/version-filter.js +107 -0
- package/dist/version-filter.js.map +1 -0
- package/dist/versioned-route.d.ts +21 -0
- package/dist/versioned-route.d.ts.map +1 -0
- package/dist/versioned-route.js +15 -0
- package/dist/versioned-route.js.map +1 -0
- package/dist/views.d.ts +42 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +110 -0
- package/dist/views.js.map +1 -0
- package/dist/ws-adapter.d.ts +81 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +214 -0
- package/dist/ws-adapter.js.map +1 -0
- package/dist/ws-client.d.ts +68 -0
- package/dist/ws-client.d.ts.map +1 -0
- package/dist/ws-client.js +135 -0
- package/dist/ws-client.js.map +1 -0
- package/dist/ws-server.d.ts +23 -0
- package/dist/ws-server.d.ts.map +1 -0
- package/dist/ws-server.js +37 -0
- package/dist/ws-server.js.map +1 -0
- package/dist/ws.d.ts +14 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +12 -0
- package/dist/ws.js.map +1 -0
- package/package.json +99 -0
- package/src/body.ts +251 -0
- package/src/bridge.ts +308 -0
- package/src/closing.ts +38 -0
- package/src/context.ts +25 -0
- package/src/cors-middleware.ts +347 -0
- package/src/handler-bridge.ts +226 -0
- package/src/hono-lifecycle.ts +259 -0
- package/src/index.ts +27 -0
- package/src/path.ts +169 -0
- package/src/query.ts +304 -0
- package/src/response-helpers.ts +60 -0
- package/src/response-writer.ts +100 -0
- package/src/route-adapter.ts +261 -0
- package/src/server-adapter.ts +294 -0
- package/src/sse.ts +274 -0
- package/src/static-assets.ts +247 -0
- package/src/version-filter.ts +170 -0
- package/src/versioned-route.ts +30 -0
- package/src/views.ts +188 -0
- package/src/ws-adapter.ts +329 -0
- package/src/ws-client.ts +190 -0
- package/src/ws-server.ts +40 -0
- package/src/ws.ts +13 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { createAdaptorServer } from '@hono/node-server';
|
|
5
|
+
import type { ServerType } from '@hono/node-server';
|
|
6
|
+
import type { NestApplicationOptions } from '@nestjs/common';
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
9
|
+
|
|
10
|
+
import { closingBridge } from './closing.ts';
|
|
11
|
+
import type { NestHono, NodeEnv } from './context.ts';
|
|
12
|
+
import { corsBridge } from './cors-middleware.ts';
|
|
13
|
+
import type { CorsOptions } from './cors-middleware.ts';
|
|
14
|
+
import { RouteAdapter } from './route-adapter.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The shutdown options, which Nest 11 does not declare as part
|
|
18
|
+
* of the application options: `return503OnClosing` was added in
|
|
19
|
+
* Nest 12, so it is read through a type that names it and stays
|
|
20
|
+
* optional for both.
|
|
21
|
+
*/
|
|
22
|
+
type ClosingOptions = NestApplicationOptions & {
|
|
23
|
+
readonly return503OnClosing?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The size a request body may reach before it is refused. */
|
|
27
|
+
const DEFAULT_BODY_LIMIT = 1_048_576;
|
|
28
|
+
|
|
29
|
+
/** The security headers Hono is configured with. */
|
|
30
|
+
type SecureHeadersOptions = NonNullable<
|
|
31
|
+
Parameters<typeof secureHeaders>[0]
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
/** What `@hono/node-server` accepts to build the Node server. */
|
|
35
|
+
type AdaptorOptions = Parameters<typeof createAdaptorServer>[0];
|
|
36
|
+
|
|
37
|
+
/** The transport options the lifecycle reads. */
|
|
38
|
+
interface TransportOptions {
|
|
39
|
+
/** Largest body accepted, in bytes; `0` accepts any size. */
|
|
40
|
+
readonly bodyLimit?: number;
|
|
41
|
+
/** Whether `req.rawBody` keeps the bytes of every body. */
|
|
42
|
+
readonly rawBody?: boolean;
|
|
43
|
+
/** The security headers to send, or `false` to send none. */
|
|
44
|
+
readonly secureHeaders?: boolean | SecureHeadersOptions;
|
|
45
|
+
/** Whether `X-Forwarded-*` headers come from a proxy. */
|
|
46
|
+
readonly trustProxy?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Destroys the connections a server is still holding open. */
|
|
50
|
+
function closeConnections(server: ServerType): void {
|
|
51
|
+
if ('closeAllConnections' in server) {
|
|
52
|
+
server.closeAllConnections();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Says whether a close failure only means the server never ran. */
|
|
57
|
+
function isNotRunning(error: unknown): boolean {
|
|
58
|
+
return (
|
|
59
|
+
typeof error === 'object' &&
|
|
60
|
+
error !== null &&
|
|
61
|
+
'code' in error &&
|
|
62
|
+
error.code === 'ERR_SERVER_NOT_RUNNING'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runs Nest on Hono: this owns the Hono application, the
|
|
68
|
+
* middleware every request passes through, and the Node server
|
|
69
|
+
* the fetch handler is served on. The routes Nest registers and
|
|
70
|
+
* the answers it writes are the business of the class below.
|
|
71
|
+
*
|
|
72
|
+
* The transport options Nest passes in are honoured: requests
|
|
73
|
+
* are served over TLS when `httpsOptions` is set, connections
|
|
74
|
+
* are dropped on shutdown when `forceCloseConnections` is set,
|
|
75
|
+
* and a request that arrives while the application is closing
|
|
76
|
+
* is answered with `503` when `return503OnClosing` is set.
|
|
77
|
+
*/
|
|
78
|
+
abstract class HonoLifecycle extends RouteAdapter {
|
|
79
|
+
protected readonly hono: NestHono;
|
|
80
|
+
protected readonly trustProxy: boolean;
|
|
81
|
+
protected bodyLimit: number;
|
|
82
|
+
protected bodyParsingEnabled = false;
|
|
83
|
+
protected rawBodyEnabled: boolean;
|
|
84
|
+
protected corsOptions: CorsOptions | undefined;
|
|
85
|
+
private closing = false;
|
|
86
|
+
private forceCloseConnections = false;
|
|
87
|
+
private return503OnClosing = false;
|
|
88
|
+
|
|
89
|
+
protected constructor(options: TransportOptions = {}) {
|
|
90
|
+
const hono = new Hono<NodeEnv>();
|
|
91
|
+
super(hono);
|
|
92
|
+
this.hono = hono;
|
|
93
|
+
this.bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
|
|
94
|
+
this.rawBodyEnabled = options.rawBody ?? false;
|
|
95
|
+
this.trustProxy = options.trustProxy ?? false;
|
|
96
|
+
this.installSecurityHeaders(options.secureHeaders ?? true);
|
|
97
|
+
// CORS runs first, so a 503 sent while closing is readable.
|
|
98
|
+
hono.use(
|
|
99
|
+
'*',
|
|
100
|
+
corsBridge(() => this.corsOptions),
|
|
101
|
+
);
|
|
102
|
+
hono.use(
|
|
103
|
+
'*',
|
|
104
|
+
closingBridge(
|
|
105
|
+
() => this.return503OnClosing && this.closing,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Identifier ecosystem packages branch on. It is the only
|
|
112
|
+
* value Nest itself does not consume.
|
|
113
|
+
*/
|
|
114
|
+
public override getType(): string {
|
|
115
|
+
return 'hono';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The Hono application behind the adapter, for the middleware
|
|
120
|
+
* and routes only Hono knows how to express. Registering on
|
|
121
|
+
* it before the application listens puts them ahead of Nest's
|
|
122
|
+
* routes, after the headers and CORS this adapter installs.
|
|
123
|
+
*/
|
|
124
|
+
public getHono(): NestHono {
|
|
125
|
+
return this.hono;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Says that a Hono router scores its routes rather than
|
|
130
|
+
* matching them in the order they were added, so two routes
|
|
131
|
+
* cannot shadow each other and Nest does not have to sort
|
|
132
|
+
* them.
|
|
133
|
+
*/
|
|
134
|
+
public isRouteOrderSensitive(): boolean {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Reads the shutdown options Nest hands the adapter. */
|
|
139
|
+
private readShutdownOptions(
|
|
140
|
+
options: NestApplicationOptions,
|
|
141
|
+
): void {
|
|
142
|
+
const closing: ClosingOptions = options;
|
|
143
|
+
this.forceCloseConnections =
|
|
144
|
+
options.forceCloseConnections ?? false;
|
|
145
|
+
this.return503OnClosing =
|
|
146
|
+
closing.return503OnClosing ?? false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates the Node server Hono's fetch handler is served
|
|
151
|
+
* through. Hono has no listener of its own, so `listen()` and
|
|
152
|
+
* `close()` operate on the value built here.
|
|
153
|
+
*/
|
|
154
|
+
public override initHttpServer(
|
|
155
|
+
options: NestApplicationOptions,
|
|
156
|
+
): void {
|
|
157
|
+
this.readShutdownOptions(options);
|
|
158
|
+
if (options.rawBody === true) {
|
|
159
|
+
this.rawBodyEnabled = true;
|
|
160
|
+
}
|
|
161
|
+
const certificate = options.httpsOptions;
|
|
162
|
+
if (certificate === undefined) {
|
|
163
|
+
this.setHttpServer(
|
|
164
|
+
createAdaptorServer({ fetch: this.hono.fetch }),
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const adaptorOptions: AdaptorOptions = {
|
|
169
|
+
createServer: createHttpsServer,
|
|
170
|
+
fetch: this.hono.fetch,
|
|
171
|
+
serverOptions: certificate,
|
|
172
|
+
};
|
|
173
|
+
this.setHttpServer(createAdaptorServer(adaptorOptions));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public override listen(
|
|
177
|
+
port: string | number,
|
|
178
|
+
callback?: () => void,
|
|
179
|
+
): void;
|
|
180
|
+
public override listen(
|
|
181
|
+
port: string | number,
|
|
182
|
+
hostname: string,
|
|
183
|
+
callback?: () => void,
|
|
184
|
+
): void;
|
|
185
|
+
public override listen(
|
|
186
|
+
port: string | number,
|
|
187
|
+
hostnameOrCallback?: string | (() => void),
|
|
188
|
+
callback?: () => void,
|
|
189
|
+
): void {
|
|
190
|
+
if (typeof port === 'string') {
|
|
191
|
+
this.httpServer.listen(port, callback);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (typeof hostnameOrCallback === 'function') {
|
|
195
|
+
this.httpServer.listen(port, hostnameOrCallback);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (typeof hostnameOrCallback === 'string') {
|
|
199
|
+
this.httpServer.listen(
|
|
200
|
+
port,
|
|
201
|
+
hostnameOrCallback,
|
|
202
|
+
callback,
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this.httpServer.listen(port, callback);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Stops the server and, when the application asked for it,
|
|
211
|
+
* the connections it is still holding open. A server that
|
|
212
|
+
* never started listening has nothing to close, so the error
|
|
213
|
+
* it reports then is not a failure worth propagating.
|
|
214
|
+
*/
|
|
215
|
+
public override async close(): Promise<void> {
|
|
216
|
+
this.closing = true;
|
|
217
|
+
const { httpServer } = this;
|
|
218
|
+
if (this.forceCloseConnections) {
|
|
219
|
+
closeConnections(httpServer);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await promisify((done: () => void) => {
|
|
223
|
+
httpServer.close(done);
|
|
224
|
+
})();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!isNotRunning(error)) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Records that the payload's bytes have to be kept. */
|
|
233
|
+
protected keepRawBody(rawBody?: boolean): void {
|
|
234
|
+
if (rawBody === true) {
|
|
235
|
+
this.rawBodyEnabled = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Installs the security headers Hono sends with every answer,
|
|
241
|
+
* unless the deployment turned them off. They come from Hono
|
|
242
|
+
* rather than from Helmet, which is Express middleware: the
|
|
243
|
+
* families of headers are the same.
|
|
244
|
+
*/
|
|
245
|
+
private installSecurityHeaders(
|
|
246
|
+
headers: boolean | SecureHeadersOptions,
|
|
247
|
+
): void {
|
|
248
|
+
if (headers === true) {
|
|
249
|
+
this.hono.use('*', secureHeaders());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (headers !== false) {
|
|
253
|
+
this.hono.use('*', secureHeaders(headers));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { HonoLifecycle };
|
|
259
|
+
export type { TransportOptions };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of the adapter: the class a bootstrap
|
|
3
|
+
* hands to `NestFactory.create`, the options it and
|
|
4
|
+
* `enableCors` accept, and the types a handler or a middleware
|
|
5
|
+
* needs to describe what it touches.
|
|
6
|
+
*/
|
|
7
|
+
export { ServerAdapter } from './server-adapter.ts';
|
|
8
|
+
export type { ParsedBody } from './body.ts';
|
|
9
|
+
export type {
|
|
10
|
+
NestHandler,
|
|
11
|
+
NestRequest,
|
|
12
|
+
NextHandler,
|
|
13
|
+
} from './bridge.ts';
|
|
14
|
+
export type {
|
|
15
|
+
NestContext,
|
|
16
|
+
NestHono,
|
|
17
|
+
NodeEnv,
|
|
18
|
+
} from './context.ts';
|
|
19
|
+
export type { CorsOptions } from './cors-middleware.ts';
|
|
20
|
+
export type { ParsedQuery } from './query.ts';
|
|
21
|
+
export type { StaticAssetsOptions } from './static-assets.ts';
|
|
22
|
+
export type {
|
|
23
|
+
ViewData,
|
|
24
|
+
ViewEngine,
|
|
25
|
+
ViewOptions,
|
|
26
|
+
} from './views.ts';
|
|
27
|
+
export type { ServerAdapterOptions } from './server-adapter.ts';
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A parameter name, which both dialects write the same way. The
|
|
3
|
+
* whole match is the name, so the expression captures nothing.
|
|
4
|
+
*/
|
|
5
|
+
const PARAMETER = /^[A-Za-z0-9_]+/u;
|
|
6
|
+
|
|
7
|
+
/** The name of a wildcard, which the v8 syntax spells `*rest`. */
|
|
8
|
+
const WILDCARD = /^[A-Za-z0-9_]+/u;
|
|
9
|
+
|
|
10
|
+
/** An optional segment, which the v8 syntax spells `{/:id}`. */
|
|
11
|
+
const OPTIONAL_SEGMENT =
|
|
12
|
+
/^:(?<name>[A-Za-z0-9_]+)(?:\((?<constraint>[^()]*)\))?\??$/u;
|
|
13
|
+
|
|
14
|
+
/** The characters this translator has no meaning for. */
|
|
15
|
+
const REFUSED = new Set(['}', '(', ')']);
|
|
16
|
+
|
|
17
|
+
const MISSING = -1;
|
|
18
|
+
|
|
19
|
+
function unsupported(path: string): TypeError {
|
|
20
|
+
return new TypeError(
|
|
21
|
+
`The route path "${path}" is not one the Hono adapter ` +
|
|
22
|
+
'can translate. Write it the way Hono reads it: ' +
|
|
23
|
+
'`:name`, `:name?`, `:name{regex}`, `{/:name}` or `*`.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function withoutLeadingSlash(content: string): string {
|
|
28
|
+
if (content.startsWith('/')) {
|
|
29
|
+
return content.slice(1);
|
|
30
|
+
}
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Translates one optional segment. Hono can only make a single
|
|
36
|
+
* parameter optional, so a group that holds a literal or more
|
|
37
|
+
* than one segment is refused instead of quietly ignored.
|
|
38
|
+
*/
|
|
39
|
+
function translateGroup(content: string, path: string): string {
|
|
40
|
+
const match = OPTIONAL_SEGMENT.exec(
|
|
41
|
+
withoutLeadingSlash(content),
|
|
42
|
+
);
|
|
43
|
+
if (match === null) {
|
|
44
|
+
throw unsupported(path);
|
|
45
|
+
}
|
|
46
|
+
const { constraint, name } = match.groups ?? {
|
|
47
|
+
constraint: undefined,
|
|
48
|
+
name: '',
|
|
49
|
+
};
|
|
50
|
+
if (constraint === undefined) {
|
|
51
|
+
return `/:${name}?`;
|
|
52
|
+
}
|
|
53
|
+
return `/:${name}{${constraint}}?`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Rewrites one route path into the dialect Hono's router reads.
|
|
58
|
+
*
|
|
59
|
+
* The two differ in three places: a constraint is written
|
|
60
|
+
* `:id(\\d+)` rather than `:id{\\d+}`, an optional segment is
|
|
61
|
+
* written `{/:id}` rather than `/:id?`, and a named wildcard is
|
|
62
|
+
* written `*rest` rather than `*`. Anything else is refused
|
|
63
|
+
* with the path that caused it, because a route that is
|
|
64
|
+
* registered but never matches is worse than one that fails at
|
|
65
|
+
* startup.
|
|
66
|
+
*/
|
|
67
|
+
class PathTranslator {
|
|
68
|
+
private index = 0;
|
|
69
|
+
private translated = '';
|
|
70
|
+
private readonly path: string;
|
|
71
|
+
|
|
72
|
+
public constructor(path: string) {
|
|
73
|
+
this.path = path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public translate(): string {
|
|
77
|
+
while (this.index < this.path.length) {
|
|
78
|
+
this.step();
|
|
79
|
+
}
|
|
80
|
+
return this.translated;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private step(): void {
|
|
84
|
+
const character = this.path.charAt(this.index);
|
|
85
|
+
if (REFUSED.has(character)) {
|
|
86
|
+
throw unsupported(this.path);
|
|
87
|
+
}
|
|
88
|
+
if (this.readSpecial(character)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.emit(character);
|
|
92
|
+
this.index += 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Copies a character, or handles the ones that mean more. */
|
|
96
|
+
private readSpecial(character: string): boolean {
|
|
97
|
+
if (character === '{') {
|
|
98
|
+
this.readGroup();
|
|
99
|
+
} else if (character === ':') {
|
|
100
|
+
this.readParameter();
|
|
101
|
+
} else if (character === '*') {
|
|
102
|
+
this.readWildcard();
|
|
103
|
+
} else {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private emit(text: string): void {
|
|
110
|
+
this.translated += text;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private readGroup(): void {
|
|
114
|
+
const end = this.path.indexOf('}', this.index);
|
|
115
|
+
if (end === MISSING) {
|
|
116
|
+
throw unsupported(this.path);
|
|
117
|
+
}
|
|
118
|
+
const content = this.path.slice(this.index + 1, end);
|
|
119
|
+
this.emit(translateGroup(content, this.path));
|
|
120
|
+
this.index = end + 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private readParameter(): void {
|
|
124
|
+
const match = PARAMETER.exec(
|
|
125
|
+
this.path.slice(this.index + 1),
|
|
126
|
+
);
|
|
127
|
+
if (match === null) {
|
|
128
|
+
this.emit(':');
|
|
129
|
+
this.index += 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const [name] = match;
|
|
133
|
+
this.index += name.length + 1;
|
|
134
|
+
this.closeParameter(name);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private closeParameter(name: string): void {
|
|
138
|
+
if (this.path.charAt(this.index) !== '(') {
|
|
139
|
+
this.emit(`:${name}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const end = this.path.indexOf(')', this.index);
|
|
143
|
+
if (end === MISSING) {
|
|
144
|
+
throw unsupported(this.path);
|
|
145
|
+
}
|
|
146
|
+
const constraint = this.path.slice(this.index + 1, end);
|
|
147
|
+
this.emit(`:${name}{${constraint}}`);
|
|
148
|
+
this.index = end + 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private readWildcard(): void {
|
|
152
|
+
const match = WILDCARD.exec(
|
|
153
|
+
this.path.slice(this.index + 1),
|
|
154
|
+
);
|
|
155
|
+
this.emit('*');
|
|
156
|
+
if (match === null) {
|
|
157
|
+
this.index += 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const [name] = match;
|
|
161
|
+
this.index += name.length + 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toHonoPath(path: string): string {
|
|
166
|
+
return new PathTranslator(path).translate();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { toHonoPath };
|