@zero-server/core 1.0.2 → 1.1.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/lib/app.js +18 -0
- package/lib/router/index.js +125 -15
- package/package.json +7 -7
package/lib/app.js
CHANGED
|
@@ -172,6 +172,24 @@ class App
|
|
|
172
172
|
* before calling `fn` so downstream sees relative paths).
|
|
173
173
|
* - `use('/prefix', router)` - mount a Router sub-app at the given prefix.
|
|
174
174
|
*
|
|
175
|
+
* Route matching order: the app matches its OWN verb routes (those declared
|
|
176
|
+
* with `app.get()`, `app.post()`, etc.) BEFORE probing any `use()`-mounted
|
|
177
|
+
* child routers. An app-level catch-all like `app.get('/*', ssr)` therefore
|
|
178
|
+
* shadows every mounted router. Give a catch-all its own child router and
|
|
179
|
+
* mount it LAST so real routes win:
|
|
180
|
+
*
|
|
181
|
+
* @example | Catch-all that does not shadow mounted routers
|
|
182
|
+
* const { createApp, Router } = require('@zero-server/sdk');
|
|
183
|
+
* const app = createApp();
|
|
184
|
+
*
|
|
185
|
+
* const api = Router();
|
|
186
|
+
* api.get('/users', listUsers);
|
|
187
|
+
* app.use('/api', api); // real routes
|
|
188
|
+
*
|
|
189
|
+
* const ssr = Router();
|
|
190
|
+
* ssr.get('/*', renderApp); // SPA / SSR fallback
|
|
191
|
+
* app.use('/', ssr); // mounted LAST - only handles what /api did not
|
|
192
|
+
*
|
|
175
193
|
* @param {string|Function} pathOrFn - A path prefix string, or middleware function.
|
|
176
194
|
* @param {Function|Router} [fn] - Middleware function or Router when first arg is a path.
|
|
177
195
|
*/
|
package/lib/router/index.js
CHANGED
|
@@ -75,6 +75,13 @@ class Router
|
|
|
75
75
|
this.routes = [];
|
|
76
76
|
/** @type {{ prefix: string, router: Router }[]} */
|
|
77
77
|
this._children = [];
|
|
78
|
+
/**
|
|
79
|
+
* Router-level middleware. Each entry runs before the handlers of any
|
|
80
|
+
* matching route in this router (and in mounted child routers), in
|
|
81
|
+
* registration order.
|
|
82
|
+
* @type {{ prefix: string|null, handler: Function }[]}
|
|
83
|
+
*/
|
|
84
|
+
this._middleware = [];
|
|
78
85
|
/**
|
|
79
86
|
* Parameter pre-processing handlers (set by parent App).
|
|
80
87
|
* @type {Object<string, Function[]>}
|
|
@@ -104,23 +111,82 @@ class Router
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
114
|
+
* Register router-level middleware or mount a child Router.
|
|
115
|
+
*
|
|
116
|
+
* Three forms are supported:
|
|
117
|
+
* - `use(fn, ...fns)` - middleware that runs before the handlers of every
|
|
118
|
+
* matching route in this router (and its mounted children).
|
|
119
|
+
* - `use(prefix, fn, ...fns)` - middleware scoped to routes whose path is at
|
|
120
|
+
* or below `prefix`.
|
|
121
|
+
* - `use(prefix, router)` - mount a child Router under a path prefix.
|
|
122
|
+
* Requests matching the prefix are delegated to the child router with the
|
|
123
|
+
* prefix stripped from `req.url`.
|
|
124
|
+
*
|
|
125
|
+
* Middleware runs as part of the matched route's handler chain, so it only
|
|
126
|
+
* executes when a route actually matches (a 404 runs no middleware). This
|
|
127
|
+
* makes it a natural fit for auth/feature guards. Unlike the earlier
|
|
128
|
+
* releases, passing a function is no longer silently ignored - an
|
|
129
|
+
* unsupported argument shape throws a `TypeError`.
|
|
110
130
|
*
|
|
111
|
-
* @param {string}
|
|
112
|
-
* @param {Router}
|
|
131
|
+
* @param {string|Function} prefixOrFn - Path prefix, or a middleware function.
|
|
132
|
+
* @param {...(Function|Router)} rest - A child `Router`, or one or more middleware functions.
|
|
113
133
|
* @returns {Router} `this` for chaining.
|
|
134
|
+
* @throws {TypeError} When the argument shape is not one of the supported forms.
|
|
114
135
|
*/
|
|
115
|
-
use(
|
|
136
|
+
use(prefixOrFn, ...rest)
|
|
116
137
|
{
|
|
117
|
-
|
|
138
|
+
// use(prefix, router) - mount a child router
|
|
139
|
+
if (typeof prefixOrFn === 'string' && rest.length === 1 && rest[0] instanceof Router)
|
|
118
140
|
{
|
|
119
|
-
const cleanPrefix =
|
|
120
|
-
this._children.push({ prefix: cleanPrefix, router });
|
|
141
|
+
const cleanPrefix = prefixOrFn.endsWith('/') ? prefixOrFn.slice(0, -1) : prefixOrFn;
|
|
142
|
+
this._children.push({ prefix: cleanPrefix, router: rest[0] });
|
|
121
143
|
log.debug('mounted child router at %s', cleanPrefix);
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// use(fn, ...fns) - global router middleware
|
|
148
|
+
if (typeof prefixOrFn === 'function')
|
|
149
|
+
{
|
|
150
|
+
for (const fn of [prefixOrFn, ...rest])
|
|
151
|
+
{
|
|
152
|
+
if (typeof fn !== 'function')
|
|
153
|
+
throw new TypeError('Router.use(): every middleware argument must be a function');
|
|
154
|
+
this._middleware.push({ prefix: null, handler: fn });
|
|
155
|
+
}
|
|
156
|
+
log.debug('registered %d router middleware', 1 + rest.length);
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// use(prefix, fn, ...fns) - prefix-scoped router middleware
|
|
161
|
+
if (typeof prefixOrFn === 'string' && rest.length > 0 && rest.every(f => typeof f === 'function'))
|
|
162
|
+
{
|
|
163
|
+
const cleanPrefix = prefixOrFn.endsWith('/') ? prefixOrFn.slice(0, -1) : prefixOrFn;
|
|
164
|
+
for (const fn of rest) this._middleware.push({ prefix: cleanPrefix, handler: fn });
|
|
165
|
+
log.debug('registered %d router middleware at %s', rest.length, cleanPrefix);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new TypeError(
|
|
170
|
+
'Router.use() expects use(fn), use(prefix, fn), or use(prefix, router)'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Collect the router-level middleware whose scope matches a path.
|
|
176
|
+
* @param {string} url - Router-local request path (query already stripped).
|
|
177
|
+
* @returns {Function[]} Applicable middleware handlers in registration order.
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
_applicableMiddleware(url)
|
|
181
|
+
{
|
|
182
|
+
if (this._middleware.length === 0) return [];
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const m of this._middleware)
|
|
185
|
+
{
|
|
186
|
+
if (m.prefix === null || url === m.prefix || url.startsWith(m.prefix + '/'))
|
|
187
|
+
out.push(m.handler);
|
|
122
188
|
}
|
|
123
|
-
return
|
|
189
|
+
return out;
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
/**
|
|
@@ -159,15 +225,20 @@ class Router
|
|
|
159
225
|
*
|
|
160
226
|
* @param {import('./request')} req - HTTP request object.
|
|
161
227
|
* @param {import('./response')} res - HTTP response object.
|
|
228
|
+
* @param {Function[]} [inherited] - Middleware inherited from parent routers,
|
|
229
|
+
* already resolved as applicable to this request.
|
|
162
230
|
* @returns {boolean} Boolean result.
|
|
163
231
|
* @private
|
|
164
232
|
*/
|
|
165
|
-
_matchAndExecute(req, res)
|
|
233
|
+
_matchAndExecute(req, res, inherited = [])
|
|
166
234
|
{
|
|
167
235
|
const method = req.method.toUpperCase();
|
|
168
236
|
const url = req.url.split('?')[0];
|
|
169
237
|
log.debug('%s %s', method, url);
|
|
170
238
|
|
|
239
|
+
// Middleware applicable at this router for the current path
|
|
240
|
+
const ownMiddleware = this._applicableMiddleware(url);
|
|
241
|
+
|
|
171
242
|
// Try own routes first
|
|
172
243
|
for (let ri = 0; ri < this.routes.length; ri++)
|
|
173
244
|
{
|
|
@@ -248,8 +319,37 @@ class Router
|
|
|
248
319
|
}
|
|
249
320
|
};
|
|
250
321
|
|
|
251
|
-
|
|
252
|
-
|
|
322
|
+
// Middleware (inherited from parents, then this router's own) runs
|
|
323
|
+
// ahead of param handlers and route handlers. A middleware that
|
|
324
|
+
// sends a response without calling next() short-circuits the chain.
|
|
325
|
+
const middleware = inherited.length || ownMiddleware.length
|
|
326
|
+
? [...inherited, ...ownMiddleware]
|
|
327
|
+
: null;
|
|
328
|
+
const startChain = () => (paramCount > 0 ? runParams() : runHandlers());
|
|
329
|
+
|
|
330
|
+
if (middleware)
|
|
331
|
+
{
|
|
332
|
+
let mIdx = 0;
|
|
333
|
+
const runMiddleware = () =>
|
|
334
|
+
{
|
|
335
|
+
if (mIdx < middleware.length)
|
|
336
|
+
{
|
|
337
|
+
const fn = middleware[mIdx++];
|
|
338
|
+
try
|
|
339
|
+
{
|
|
340
|
+
const result = fn(req, res, runMiddleware);
|
|
341
|
+
if (result && typeof result.catch === 'function')
|
|
342
|
+
{
|
|
343
|
+
result.catch(e => this._handleRouteError(e, req, res));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (e) { this._handleRouteError(e, req, res); }
|
|
347
|
+
}
|
|
348
|
+
else { startChain(); }
|
|
349
|
+
};
|
|
350
|
+
runMiddleware();
|
|
351
|
+
}
|
|
352
|
+
else { startChain(); }
|
|
253
353
|
return true;
|
|
254
354
|
}
|
|
255
355
|
|
|
@@ -262,11 +362,21 @@ class Router
|
|
|
262
362
|
const origUrl = req.url;
|
|
263
363
|
const origBaseUrl = req.baseUrl || '';
|
|
264
364
|
req.baseUrl = origBaseUrl + child.prefix;
|
|
265
|
-
|
|
365
|
+
// Keep the query string attached to a normalized path: a
|
|
366
|
+
// request for exactly the mount prefix plus a query
|
|
367
|
+
// ('/api/files?x=1') must delegate as '/?x=1', not '?x=1'.
|
|
368
|
+
const rest = req.url.slice(child.prefix.length);
|
|
369
|
+
req.url = rest === '' || rest.startsWith('?') ? '/' + rest : rest;
|
|
266
370
|
child.router._paramHandlers = this._paramHandlers;
|
|
371
|
+
// Carry this router's applicable middleware down so mounted
|
|
372
|
+
// routes are guarded too. Applicability is resolved here, against
|
|
373
|
+
// the parent-relative URL, before the prefix is stripped.
|
|
374
|
+
const childInherited = inherited.length || ownMiddleware.length
|
|
375
|
+
? [...inherited, ...ownMiddleware]
|
|
376
|
+
: inherited;
|
|
267
377
|
try
|
|
268
378
|
{
|
|
269
|
-
const found = child.router._matchAndExecute(req, res);
|
|
379
|
+
const found = child.router._matchAndExecute(req, res, childInherited);
|
|
270
380
|
if (found) return true;
|
|
271
381
|
}
|
|
272
382
|
catch (e) { this._handleRouteError(e, req, res); return true; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zero-server/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "App factory, Router, and the HTTP Request/Response wrappers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zero-server",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
},
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@zero-server/realtime": "1.0
|
|
49
|
-
"@zero-server/grpc": "1.0
|
|
50
|
-
"@zero-server/lifecycle": "1.0
|
|
51
|
-
"@zero-server/observe": "1.0
|
|
52
|
-
"@zero-server/auth": "1.0
|
|
48
|
+
"@zero-server/realtime": "1.1.0",
|
|
49
|
+
"@zero-server/grpc": "1.1.0",
|
|
50
|
+
"@zero-server/lifecycle": "1.1.0",
|
|
51
|
+
"@zero-server/observe": "1.1.0",
|
|
52
|
+
"@zero-server/auth": "1.1.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@zero-server/sdk": ">=1.0
|
|
55
|
+
"@zero-server/sdk": ">=1.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
58
58
|
"@zero-server/sdk": {
|