jsgui3-server 0.0.151 → 0.0.152
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/README.md +21 -0
- package/admin-ui/v1/controls/admin_shell.js +33 -0
- package/admin-ui/v1/server.js +14 -1
- package/docs/api-reference.md +120 -2
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/configuration-reference.md +54 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/json-body.js +126 -0
- package/openapi.js +474 -0
- package/package.json +11 -8
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +728 -18
- package/server.js +421 -103
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- package/tests/test-runner.js +1 -0
package/serve-factory.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
ensure_route_leading_slash
|
|
7
7
|
} = require('./serve-helpers');
|
|
8
8
|
const lib_path = require('path');
|
|
9
|
+
const Website = require('./website/website');
|
|
9
10
|
const Webpage = require('./website/webpage');
|
|
10
11
|
const HTTP_Webpage_Publisher = require('./publishers/http-webpage-publisher');
|
|
11
12
|
const HTTP_SSE_Publisher = require('./publishers/http-sse-publisher');
|
|
@@ -14,6 +15,35 @@ const Process_Resource = require('./resources/process-resource');
|
|
|
14
15
|
const Remote_Process_Resource = require('./resources/remote-process-resource');
|
|
15
16
|
const { get_port_or_free } = require('./port-utils');
|
|
16
17
|
|
|
18
|
+
const website_marker = Symbol.for('jsgui3.website');
|
|
19
|
+
const webpage_marker = Symbol.for('jsgui3.webpage');
|
|
20
|
+
|
|
21
|
+
const strip_trailing_slash = (route_value) => {
|
|
22
|
+
if (!route_value) {
|
|
23
|
+
return '/';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let normalized_route = ensure_route_leading_slash(String(route_value));
|
|
27
|
+
while (normalized_route.length > 1 && normalized_route.endsWith('/')) {
|
|
28
|
+
normalized_route = normalized_route.slice(0, -1);
|
|
29
|
+
}
|
|
30
|
+
return normalized_route;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const normalize_route_path = (route_value, fallback_route = '/') => {
|
|
34
|
+
const route_candidate = route_value || fallback_route || '/';
|
|
35
|
+
return strip_trailing_slash(route_candidate);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalize_base_path = (base_path) => {
|
|
39
|
+
if (!base_path) {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const normalized_base_path = normalize_route_path(base_path, '/');
|
|
44
|
+
return normalized_base_path === '/' ? '' : normalized_base_path;
|
|
45
|
+
};
|
|
46
|
+
|
|
17
47
|
const prepare_webpage_route = (server, route, page_options = {}, defaults = {}) => {
|
|
18
48
|
return new Promise((resolve, reject) => {
|
|
19
49
|
try {
|
|
@@ -49,9 +79,14 @@ const prepare_webpage_route = (server, route, page_options = {}, defaults = {})
|
|
|
49
79
|
webpage_publisher.on('ready', (bundle) => {
|
|
50
80
|
try {
|
|
51
81
|
if (bundle && bundle._arr) {
|
|
82
|
+
const target_router = server.router || server.server_router;
|
|
83
|
+
if (!target_router || typeof target_router.set_route !== 'function') {
|
|
84
|
+
reject(new Error(`Server router is unavailable while preparing route ${route}`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
52
87
|
for (const item of bundle._arr) {
|
|
53
88
|
const static_responder = new Static_Route_HTTP_Responder(item);
|
|
54
|
-
|
|
89
|
+
target_router.set_route(item.route, static_responder, static_responder.handle_http);
|
|
55
90
|
}
|
|
56
91
|
resolve();
|
|
57
92
|
return;
|
|
@@ -197,8 +232,404 @@ const normalize_resource_entries = (resources_option) => {
|
|
|
197
232
|
throw new Error('`resources` must be an object map or an array.');
|
|
198
233
|
};
|
|
199
234
|
|
|
235
|
+
const is_plain_object = (value) => {
|
|
236
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const is_website_like = (value) => {
|
|
240
|
+
if (!value || typeof value !== 'object') {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (value[website_marker] === true) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (value instanceof Website) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const has_pages_collection = value.pages !== undefined || value._pages !== undefined;
|
|
253
|
+
if (has_pages_collection) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return typeof value.add_page === 'function'
|
|
258
|
+
&& typeof value.get_page === 'function';
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const is_webpage_like = (value) => {
|
|
262
|
+
if (!value || typeof value !== 'object') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (value[webpage_marker] === true) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (value instanceof Webpage) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return typeof value.path === 'string'
|
|
275
|
+
&& (
|
|
276
|
+
typeof value.ctrl === 'function'
|
|
277
|
+
|| typeof value.Ctrl === 'function'
|
|
278
|
+
|| typeof value.content === 'function'
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const extract_page_ctrl = (page_spec = {}) => {
|
|
283
|
+
const candidate_ctrl = page_spec.ctrl || page_spec.Ctrl;
|
|
284
|
+
if (candidate_ctrl !== undefined) {
|
|
285
|
+
return candidate_ctrl;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (typeof page_spec.content === 'function') {
|
|
289
|
+
return page_spec.content;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return undefined;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const normalize_page_entry = (page_spec = {}, fallback_route = '/') => {
|
|
296
|
+
const source_spec = typeof page_spec === 'function'
|
|
297
|
+
? { ctrl: page_spec }
|
|
298
|
+
: (is_plain_object(page_spec) ? { ...page_spec } : {});
|
|
299
|
+
|
|
300
|
+
const route_value = source_spec.path || source_spec.route || fallback_route || '/';
|
|
301
|
+
const route = normalize_route_path(route_value, '/');
|
|
302
|
+
|
|
303
|
+
const content_ctrl = extract_page_ctrl(source_spec);
|
|
304
|
+
const normalized_page = {
|
|
305
|
+
...source_spec,
|
|
306
|
+
path: route,
|
|
307
|
+
route,
|
|
308
|
+
ctrl: content_ctrl,
|
|
309
|
+
Ctrl: content_ctrl,
|
|
310
|
+
content: content_ctrl
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (source_spec.content !== undefined && typeof source_spec.content !== 'function') {
|
|
314
|
+
normalized_page.content_data = source_spec.content;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return [route, normalized_page];
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const normalize_website_pages = (website_value) => {
|
|
321
|
+
const normalized_pages = [];
|
|
322
|
+
const push_page_entry = (page_entry, fallback_route) => {
|
|
323
|
+
normalized_pages.push(normalize_page_entry(page_entry, fallback_route));
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (Array.isArray(website_value.pages)) {
|
|
327
|
+
for (const page_entry of website_value.pages) {
|
|
328
|
+
push_page_entry(page_entry, page_entry && page_entry.path ? page_entry.path : '/');
|
|
329
|
+
}
|
|
330
|
+
return normalized_pages;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (website_value.pages instanceof Map) {
|
|
334
|
+
for (const [route_key, page_entry] of website_value.pages.entries()) {
|
|
335
|
+
push_page_entry(page_entry, route_key);
|
|
336
|
+
}
|
|
337
|
+
return normalized_pages;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (website_value.pages && Array.isArray(website_value.pages._arr)) {
|
|
341
|
+
for (const page_entry of website_value.pages._arr) {
|
|
342
|
+
push_page_entry(page_entry, page_entry && page_entry.path ? page_entry.path : '/');
|
|
343
|
+
}
|
|
344
|
+
return normalized_pages;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (website_value._pages instanceof Map) {
|
|
348
|
+
for (const [route_key, page_entry] of website_value._pages.entries()) {
|
|
349
|
+
push_page_entry(page_entry, route_key);
|
|
350
|
+
}
|
|
351
|
+
return normalized_pages;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (is_plain_object(website_value.pages)) {
|
|
355
|
+
for (const [route_key, page_entry] of Object.entries(website_value.pages)) {
|
|
356
|
+
push_page_entry(page_entry, route_key);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return normalized_pages;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const normalize_endpoint_entry = (endpoint_name, endpoint_value = {}, default_base_path = '') => {
|
|
364
|
+
const normalized_base_path = normalize_base_path(default_base_path);
|
|
365
|
+
const resolve_default_endpoint_path = (route_name) => {
|
|
366
|
+
if (typeof route_name === 'string' && route_name.startsWith('/')) {
|
|
367
|
+
return join_base_path(normalized_base_path, route_name);
|
|
368
|
+
}
|
|
369
|
+
if (typeof route_name === 'string' && route_name.length > 0) {
|
|
370
|
+
return join_base_path(normalized_base_path, `/api/${route_name}`);
|
|
371
|
+
}
|
|
372
|
+
return join_base_path(normalized_base_path, '/api');
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
if (typeof endpoint_value === 'function') {
|
|
376
|
+
if (typeof endpoint_name !== 'string' || endpoint_name.length === 0) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
name: endpoint_name,
|
|
381
|
+
handler: endpoint_value,
|
|
382
|
+
method: 'GET',
|
|
383
|
+
path: resolve_default_endpoint_path(endpoint_name)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!is_plain_object(endpoint_value) || typeof endpoint_value.handler !== 'function') {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const endpoint_label = endpoint_value.name || endpoint_name;
|
|
392
|
+
if (!endpoint_value.path && (typeof endpoint_label !== 'string' || endpoint_label.length === 0)) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const endpoint_method = endpoint_value.method || 'GET';
|
|
397
|
+
const endpoint_path = endpoint_value.path
|
|
398
|
+
? normalize_route_path(endpoint_value.path, '/')
|
|
399
|
+
: resolve_default_endpoint_path(endpoint_label);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
name: endpoint_label,
|
|
403
|
+
handler: endpoint_value.handler,
|
|
404
|
+
method: endpoint_method,
|
|
405
|
+
path: endpoint_path,
|
|
406
|
+
description: endpoint_value.description,
|
|
407
|
+
// Extended API metadata for OpenAPI / Swagger generation.
|
|
408
|
+
summary: endpoint_value.summary,
|
|
409
|
+
tags: endpoint_value.tags,
|
|
410
|
+
params: endpoint_value.params,
|
|
411
|
+
returns: endpoint_value.returns,
|
|
412
|
+
schema: endpoint_value.schema,
|
|
413
|
+
// Enhancement support: raw handler, deprecated, operationId.
|
|
414
|
+
raw: endpoint_value.raw,
|
|
415
|
+
deprecated: endpoint_value.deprecated,
|
|
416
|
+
operationId: endpoint_value.operationId
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const normalize_website_endpoints = (website_value, normalized_base_path = '') => {
|
|
421
|
+
const normalized_endpoints = [];
|
|
422
|
+
const push_endpoint = (endpoint_entry, endpoint_name) => {
|
|
423
|
+
if (!endpoint_entry) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (typeof endpoint_entry === 'function') {
|
|
428
|
+
if (typeof endpoint_name !== 'string' || endpoint_name.length === 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const normalized_endpoint = normalize_endpoint_entry(endpoint_name, endpoint_entry, normalized_base_path);
|
|
432
|
+
if (normalized_endpoint) {
|
|
433
|
+
normalized_endpoints.push(normalized_endpoint);
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (is_plain_object(endpoint_entry) && typeof endpoint_entry.handler === 'function') {
|
|
439
|
+
const normalized_endpoint = normalize_endpoint_entry(
|
|
440
|
+
endpoint_name || endpoint_entry.name,
|
|
441
|
+
endpoint_entry,
|
|
442
|
+
normalized_base_path
|
|
443
|
+
);
|
|
444
|
+
if (normalized_endpoint) {
|
|
445
|
+
normalized_endpoints.push(normalized_endpoint);
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (is_plain_object(endpoint_entry) && typeof endpoint_entry.publish === 'function') {
|
|
451
|
+
const normalized_endpoint = normalize_endpoint_entry(endpoint_name, endpoint_entry.publish, normalized_base_path);
|
|
452
|
+
if (!normalized_endpoint) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
normalized_endpoints.push(normalized_endpoint);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
if (Array.isArray(website_value.api_endpoints)) {
|
|
460
|
+
for (const endpoint_entry of website_value.api_endpoints) {
|
|
461
|
+
push_endpoint(endpoint_entry, endpoint_entry && endpoint_entry.name);
|
|
462
|
+
}
|
|
463
|
+
return normalized_endpoints;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (website_value._api instanceof Map) {
|
|
467
|
+
for (const [endpoint_name, endpoint_entry] of website_value._api.entries()) {
|
|
468
|
+
push_endpoint(
|
|
469
|
+
is_plain_object(endpoint_entry)
|
|
470
|
+
? { name: endpoint_name, ...endpoint_entry }
|
|
471
|
+
: endpoint_entry,
|
|
472
|
+
endpoint_name
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return normalized_endpoints;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (website_value.api && typeof website_value.api[Symbol.iterator] === 'function' && !is_plain_object(website_value.api)) {
|
|
479
|
+
for (const endpoint_entry of website_value.api) {
|
|
480
|
+
if (Array.isArray(endpoint_entry)) {
|
|
481
|
+
const [endpoint_name, endpoint_value] = endpoint_entry;
|
|
482
|
+
const normalized_endpoint = normalize_endpoint_entry(endpoint_name, endpoint_value, normalized_base_path);
|
|
483
|
+
if (normalized_endpoint) {
|
|
484
|
+
normalized_endpoints.push(normalized_endpoint);
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
push_endpoint(endpoint_entry, endpoint_entry && endpoint_entry.name);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return normalized_endpoints;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (is_plain_object(website_value.api)) {
|
|
494
|
+
for (const [endpoint_name, endpoint_value] of Object.entries(website_value.api)) {
|
|
495
|
+
const normalized_endpoint = normalize_endpoint_entry(endpoint_name, endpoint_value, normalized_base_path);
|
|
496
|
+
if (normalized_endpoint) {
|
|
497
|
+
normalized_endpoints.push(normalized_endpoint);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return normalized_endpoints;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const join_base_path = (base_path, route_path) => {
|
|
506
|
+
const normalized_route = normalize_route_path(route_path || '/', '/');
|
|
507
|
+
const normalized_base = normalize_base_path(base_path);
|
|
508
|
+
|
|
509
|
+
if (!normalized_base) {
|
|
510
|
+
return normalized_route;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (normalized_route === '/') {
|
|
514
|
+
return normalized_base;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return `${normalized_base}${normalized_route}`;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const dedupe_normalized_endpoints = (normalized_endpoints = []) => {
|
|
521
|
+
const deduped_endpoints = [];
|
|
522
|
+
const seen_endpoint_keys = new Set();
|
|
523
|
+
|
|
524
|
+
for (const endpoint of normalized_endpoints) {
|
|
525
|
+
if (!endpoint || typeof endpoint.handler !== 'function') {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const endpoint_method = String(endpoint.method || 'GET').toUpperCase();
|
|
530
|
+
const endpoint_path = endpoint.path
|
|
531
|
+
? normalize_route_path(endpoint.path, '/')
|
|
532
|
+
: (
|
|
533
|
+
typeof endpoint.name === 'string' && endpoint.name.length > 0
|
|
534
|
+
? endpoint.name
|
|
535
|
+
: ''
|
|
536
|
+
);
|
|
537
|
+
const endpoint_key = `${endpoint_method} ${endpoint_path}`;
|
|
538
|
+
if (seen_endpoint_keys.has(endpoint_key)) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
seen_endpoint_keys.add(endpoint_key);
|
|
543
|
+
deduped_endpoints.push({
|
|
544
|
+
...endpoint,
|
|
545
|
+
method: endpoint_method,
|
|
546
|
+
path: endpoint.path ? normalize_route_path(endpoint.path, '/') : endpoint.path
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return deduped_endpoints;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const normalize_api_endpoints_from_options = (serve_options = {}, base_path = '') => {
|
|
554
|
+
const collected_endpoints = [];
|
|
555
|
+
|
|
556
|
+
if (Array.isArray(serve_options.api_endpoints)) {
|
|
557
|
+
collected_endpoints.push(...normalize_website_endpoints({
|
|
558
|
+
api_endpoints: serve_options.api_endpoints
|
|
559
|
+
}, base_path));
|
|
560
|
+
} else if (is_plain_object(serve_options.api_endpoints)) {
|
|
561
|
+
collected_endpoints.push(...normalize_website_endpoints({
|
|
562
|
+
api: serve_options.api_endpoints
|
|
563
|
+
}, base_path));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (serve_options.api && typeof serve_options.api === 'object') {
|
|
567
|
+
collected_endpoints.push(...normalize_website_endpoints({
|
|
568
|
+
api: serve_options.api
|
|
569
|
+
}, base_path));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return dedupe_normalized_endpoints(collected_endpoints);
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const get_manifest_candidate = (input_value, serve_options = {}) => {
|
|
576
|
+
const input_manifest = normalize_serve_input(input_value);
|
|
577
|
+
if (input_manifest) {
|
|
578
|
+
return input_manifest;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (serve_options && typeof serve_options === 'object') {
|
|
582
|
+
const explicit_webpage = normalize_serve_input(serve_options.webpage);
|
|
583
|
+
if (explicit_webpage) {
|
|
584
|
+
return explicit_webpage;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const explicit_website = normalize_serve_input(serve_options.website);
|
|
588
|
+
if (explicit_website) {
|
|
589
|
+
return explicit_website;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return null;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const normalize_serve_input = (input_value) => {
|
|
597
|
+
if (is_webpage_like(input_value)) {
|
|
598
|
+
const [route, page_config] = normalize_page_entry(input_value, input_value.path || '/');
|
|
599
|
+
return {
|
|
600
|
+
source: 'webpage',
|
|
601
|
+
name: input_value.name,
|
|
602
|
+
meta: input_value.meta || {},
|
|
603
|
+
assets: input_value.assets || {},
|
|
604
|
+
pages: [[route, page_config]],
|
|
605
|
+
api_endpoints: []
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (is_website_like(input_value)) {
|
|
610
|
+
const base_path = normalize_base_path(input_value.base_path);
|
|
611
|
+
const normalized_pages = normalize_website_pages(input_value).map(([route, page_config]) => {
|
|
612
|
+
const route_with_base = join_base_path(base_path, route);
|
|
613
|
+
return [route_with_base, { ...page_config, path: route_with_base, route: route_with_base }];
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const normalized_endpoints = normalize_website_endpoints(input_value, base_path);
|
|
617
|
+
return {
|
|
618
|
+
source: 'website',
|
|
619
|
+
name: input_value.name,
|
|
620
|
+
meta: input_value.meta || {},
|
|
621
|
+
assets: input_value.assets || {},
|
|
622
|
+
base_path: base_path || undefined,
|
|
623
|
+
pages: normalized_pages,
|
|
624
|
+
api_endpoints: normalized_endpoints
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return null;
|
|
629
|
+
};
|
|
630
|
+
|
|
200
631
|
module.exports = (Server) => {
|
|
201
|
-
const serve = function(input, maybe_options, maybe_callback) {
|
|
632
|
+
const serve = function (input, maybe_options, maybe_callback) {
|
|
202
633
|
let callback = null;
|
|
203
634
|
if (typeof maybe_options === 'function') {
|
|
204
635
|
callback = maybe_options;
|
|
@@ -225,6 +656,65 @@ module.exports = (Server) => {
|
|
|
225
656
|
};
|
|
226
657
|
}
|
|
227
658
|
|
|
659
|
+
const has_explicit_page_overrides = !!(
|
|
660
|
+
maybe_options
|
|
661
|
+
&& typeof maybe_options === 'object'
|
|
662
|
+
&& (
|
|
663
|
+
maybe_options.page !== undefined
|
|
664
|
+
|| maybe_options.pages !== undefined
|
|
665
|
+
|| maybe_options.ctrl !== undefined
|
|
666
|
+
|| maybe_options.Ctrl !== undefined
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const normalized_input_manifest = get_manifest_candidate(input, serve_options);
|
|
671
|
+
if (normalized_input_manifest) {
|
|
672
|
+
if (!serve_options.name && normalized_input_manifest.name) {
|
|
673
|
+
serve_options.name = normalized_input_manifest.name;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (
|
|
677
|
+
!has_explicit_page_overrides
|
|
678
|
+
&& !serve_options.page
|
|
679
|
+
&& Array.isArray(normalized_input_manifest.pages)
|
|
680
|
+
&& normalized_input_manifest.pages.length
|
|
681
|
+
) {
|
|
682
|
+
const normalized_pages_map = {};
|
|
683
|
+
const seen_routes = new Set();
|
|
684
|
+
for (const [route, page_config] of normalized_input_manifest.pages) {
|
|
685
|
+
const normalized_route = normalize_route_path(route, '/');
|
|
686
|
+
if (seen_routes.has(normalized_route)) {
|
|
687
|
+
throw new Error(`duplicate_route: ${normalized_route}`);
|
|
688
|
+
}
|
|
689
|
+
seen_routes.add(normalized_route);
|
|
690
|
+
normalized_pages_map[normalized_route] = page_config || {};
|
|
691
|
+
}
|
|
692
|
+
serve_options.pages = normalized_pages_map;
|
|
693
|
+
|
|
694
|
+
if (
|
|
695
|
+
normalized_input_manifest.source === 'webpage'
|
|
696
|
+
|| normalized_input_manifest.source === 'website'
|
|
697
|
+
) {
|
|
698
|
+
delete serve_options.ctrl;
|
|
699
|
+
delete serve_options.Ctrl;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (
|
|
704
|
+
!serve_options.api
|
|
705
|
+
&& !serve_options.api_endpoints
|
|
706
|
+
&& Array.isArray(normalized_input_manifest.api_endpoints)
|
|
707
|
+
&& normalized_input_manifest.api_endpoints.length
|
|
708
|
+
) {
|
|
709
|
+
serve_options.api_endpoints = normalized_input_manifest.api_endpoints;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const manifest_base_path = normalize_base_path(
|
|
714
|
+
(normalized_input_manifest && normalized_input_manifest.base_path)
|
|
715
|
+
|| serve_options.base_path
|
|
716
|
+
);
|
|
717
|
+
|
|
228
718
|
const caller_file = serve_options.caller_file || serve_options.callerFile || guess_caller_file();
|
|
229
719
|
const caller_dir = serve_options.root
|
|
230
720
|
? lib_path.resolve(process.cwd(), serve_options.root)
|
|
@@ -234,29 +724,47 @@ module.exports = (Server) => {
|
|
|
234
724
|
if (!serve_options.ctrl && serve_options.page) {
|
|
235
725
|
const page_config = serve_options.page;
|
|
236
726
|
serve_options.ctrl = page_config.content || page_config.ctrl || page_config.Ctrl;
|
|
237
|
-
serve_options.page_route =
|
|
727
|
+
serve_options.page_route = normalize_route_path(page_config.route || page_config.path || '/', '/');
|
|
238
728
|
serve_options.page_config = page_config;
|
|
239
729
|
}
|
|
240
730
|
|
|
241
731
|
let additional_pages = [];
|
|
732
|
+
let use_manual_page_publication = false;
|
|
733
|
+
if (!serve_options.ctrl && Array.isArray(serve_options.pages)) {
|
|
734
|
+
throw new Error('`pages` option must be an object map of route -> page config.');
|
|
735
|
+
}
|
|
736
|
+
|
|
242
737
|
if (!serve_options.ctrl && serve_options.pages && typeof serve_options.pages === 'object') {
|
|
243
738
|
const page_entries = Object.entries(serve_options.pages);
|
|
244
739
|
if (!page_entries.length) {
|
|
245
740
|
throw new Error('`pages` option requires at least one entry.');
|
|
246
741
|
}
|
|
247
742
|
|
|
248
|
-
const normalized_pages = page_entries.map(([route, cfg]) => [
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
743
|
+
const normalized_pages = page_entries.map(([route, cfg]) => [normalize_route_path(route, '/'), cfg || {}]);
|
|
744
|
+
const seen_page_routes = new Set();
|
|
745
|
+
for (const [route] of normalized_pages) {
|
|
746
|
+
if (seen_page_routes.has(route)) {
|
|
747
|
+
throw new Error(`duplicate_route: ${route}`);
|
|
748
|
+
}
|
|
749
|
+
seen_page_routes.add(route);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const root_entry = normalized_pages.find(([route]) => route === '/');
|
|
753
|
+
if (root_entry) {
|
|
754
|
+
serve_options.ctrl = (root_entry[1].content || root_entry[1].ctrl || root_entry[1].Ctrl);
|
|
755
|
+
serve_options.page_route = root_entry[0];
|
|
756
|
+
serve_options.page_config = root_entry[1];
|
|
757
|
+
additional_pages = normalized_pages.filter(([route]) => route !== serve_options.page_route);
|
|
758
|
+
} else {
|
|
759
|
+
use_manual_page_publication = true;
|
|
760
|
+
additional_pages = normalized_pages;
|
|
761
|
+
}
|
|
254
762
|
}
|
|
255
763
|
|
|
256
764
|
const explicit_client_path = serve_options.clientPath || serve_options.client_path || serve_options.src_path_client_js || serve_options.disk_path_client_js;
|
|
257
765
|
const root_client_path = find_default_client_path(explicit_client_path, caller_dir);
|
|
258
766
|
|
|
259
|
-
if (typeof serve_options.ctrl !== 'function' && root_client_path) {
|
|
767
|
+
if (typeof serve_options.ctrl !== 'function' && root_client_path && !use_manual_page_publication) {
|
|
260
768
|
const auto_ctrl = load_default_control_from_client(root_client_path);
|
|
261
769
|
if (typeof auto_ctrl === 'function') {
|
|
262
770
|
serve_options.ctrl = auto_ctrl;
|
|
@@ -266,10 +774,22 @@ module.exports = (Server) => {
|
|
|
266
774
|
if (serve_options.page_config && typeof serve_options.ctrl !== 'function') {
|
|
267
775
|
throw new Error('`page` option requires a control constructor.');
|
|
268
776
|
}
|
|
269
|
-
if (additional_pages.length && typeof serve_options.ctrl !== 'function') {
|
|
777
|
+
if (additional_pages.length && !use_manual_page_publication && typeof serve_options.ctrl !== 'function') {
|
|
270
778
|
throw new Error('`pages` option requires at least one control constructor.');
|
|
271
779
|
}
|
|
272
780
|
|
|
781
|
+
if (use_manual_page_publication) {
|
|
782
|
+
const invalid_page_entry = additional_pages.find(([, cfg]) => {
|
|
783
|
+
const page_ctrl = cfg && (cfg.content || cfg.ctrl || cfg.Ctrl);
|
|
784
|
+
return typeof page_ctrl !== 'function';
|
|
785
|
+
});
|
|
786
|
+
if (invalid_page_entry) {
|
|
787
|
+
throw new Error(`Page at route "${invalid_page_entry[0]}" requires a control constructor as content.`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const normalized_api_endpoints = normalize_api_endpoints_from_options(serve_options, manifest_base_path);
|
|
792
|
+
|
|
273
793
|
const port = Number.isFinite(serve_options.port)
|
|
274
794
|
? Number(serve_options.port)
|
|
275
795
|
: (serve_options.port === 'auto' ? 0 : (process.env.PORT ? Number(process.env.PORT) : 8080));
|
|
@@ -293,7 +813,11 @@ module.exports = (Server) => {
|
|
|
293
813
|
|
|
294
814
|
if (typeof serve_options.ctrl === 'function') {
|
|
295
815
|
server_spec.Ctrl = serve_options.ctrl;
|
|
296
|
-
} else if (
|
|
816
|
+
} else if (
|
|
817
|
+
(serve_options.api && typeof serve_options.api === 'object')
|
|
818
|
+
|| normalized_api_endpoints.length
|
|
819
|
+
|| use_manual_page_publication
|
|
820
|
+
) {
|
|
297
821
|
server_spec.website = false;
|
|
298
822
|
}
|
|
299
823
|
|
|
@@ -383,6 +907,85 @@ module.exports = (Server) => {
|
|
|
383
907
|
? Number(serve_options.readyTimeoutMs)
|
|
384
908
|
: 120000;
|
|
385
909
|
|
|
910
|
+
const manifest_page_entries = [];
|
|
911
|
+
const seen_manifest_routes = new Set();
|
|
912
|
+
const push_manifest_page = (route, page_config = {}) => {
|
|
913
|
+
const normalized_route = normalize_route_path(route, '/');
|
|
914
|
+
if (seen_manifest_routes.has(normalized_route)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
seen_manifest_routes.add(normalized_route);
|
|
918
|
+
manifest_page_entries.push([normalized_route, page_config || {}]);
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
if (normalized_input_manifest && Array.isArray(normalized_input_manifest.pages) && normalized_input_manifest.pages.length) {
|
|
922
|
+
for (const [route, page_config] of normalized_input_manifest.pages) {
|
|
923
|
+
push_manifest_page(route, page_config);
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
if (serve_options.page_config && serve_options.page_route) {
|
|
927
|
+
push_manifest_page(serve_options.page_route, serve_options.page_config);
|
|
928
|
+
}
|
|
929
|
+
for (const [route, page_config] of additional_pages) {
|
|
930
|
+
push_manifest_page(route, page_config);
|
|
931
|
+
}
|
|
932
|
+
if (!manifest_page_entries.length && typeof serve_options.ctrl === 'function' && !use_manual_page_publication) {
|
|
933
|
+
push_manifest_page('/', {
|
|
934
|
+
ctrl: serve_options.ctrl,
|
|
935
|
+
content: serve_options.ctrl,
|
|
936
|
+
name: serve_options.name
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const manifest_warning_messages = [];
|
|
942
|
+
const non_get_endpoints = normalized_api_endpoints.filter((endpoint) => endpoint.method !== 'GET');
|
|
943
|
+
if (non_get_endpoints.length) {
|
|
944
|
+
manifest_warning_messages.push(
|
|
945
|
+
'API endpoint metadata includes non-GET methods, but server.publish currently treats handlers as method-agnostic.'
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const effective_website_manifest = {
|
|
950
|
+
source: (normalized_input_manifest && normalized_input_manifest.source)
|
|
951
|
+
|| (use_manual_page_publication ? 'pages' : (typeof serve_options.ctrl === 'function' ? 'ctrl' : 'legacy')),
|
|
952
|
+
name: serve_options.name || server_spec.name || 'jsgui3 server',
|
|
953
|
+
base_path: manifest_base_path || undefined,
|
|
954
|
+
meta: (normalized_input_manifest && normalized_input_manifest.meta) || serve_options.meta || {},
|
|
955
|
+
assets: (normalized_input_manifest && normalized_input_manifest.assets) || serve_options.assets || {},
|
|
956
|
+
pages: manifest_page_entries.map(([route, page_config]) => {
|
|
957
|
+
const page_ctrl = extract_page_ctrl(page_config || {});
|
|
958
|
+
return {
|
|
959
|
+
route,
|
|
960
|
+
path: route,
|
|
961
|
+
name: page_config ? page_config.name : undefined,
|
|
962
|
+
title: page_config ? page_config.title : undefined,
|
|
963
|
+
render_mode: (page_config && page_config.render_mode)
|
|
964
|
+
|| (typeof page_ctrl === 'function' ? 'dynamic' : 'static'),
|
|
965
|
+
has_ctrl: typeof page_ctrl === 'function'
|
|
966
|
+
};
|
|
967
|
+
}),
|
|
968
|
+
api_endpoints: normalized_api_endpoints.map((endpoint) => ({
|
|
969
|
+
name: endpoint.name,
|
|
970
|
+
method: endpoint.method || 'GET',
|
|
971
|
+
path: endpoint.path,
|
|
972
|
+
description: endpoint.description,
|
|
973
|
+
summary: endpoint.summary,
|
|
974
|
+
tags: endpoint.tags,
|
|
975
|
+
params: endpoint.params,
|
|
976
|
+
returns: endpoint.returns,
|
|
977
|
+
schema: endpoint.schema
|
|
978
|
+
}))
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
server_instance.website_manifest = effective_website_manifest;
|
|
982
|
+
server_instance.publication_summary = {
|
|
983
|
+
source: effective_website_manifest.source,
|
|
984
|
+
page_routes: effective_website_manifest.pages.map((page) => page.path),
|
|
985
|
+
api_routes: effective_website_manifest.api_endpoints.map((endpoint) => `${endpoint.method} ${endpoint.path}`),
|
|
986
|
+
warnings: manifest_warning_messages
|
|
987
|
+
};
|
|
988
|
+
|
|
386
989
|
const extra_page_promises = additional_pages.map(([route, cfg]) => prepare_webpage_route(server_instance, route, cfg, {
|
|
387
990
|
caller_dir,
|
|
388
991
|
debug: debug_enabled,
|
|
@@ -399,14 +1002,114 @@ module.exports = (Server) => {
|
|
|
399
1002
|
}));
|
|
400
1003
|
}
|
|
401
1004
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1005
|
+
// ── Register middleware ──────────────────────────────
|
|
1006
|
+
// `middleware` accepts an array of (req, res, next) functions.
|
|
1007
|
+
if (Array.isArray(serve_options.middleware)) {
|
|
1008
|
+
for (const mw of serve_options.middleware) {
|
|
1009
|
+
if (typeof mw === 'function') {
|
|
1010
|
+
server_instance.use(mw);
|
|
406
1011
|
}
|
|
407
1012
|
}
|
|
408
1013
|
}
|
|
409
1014
|
|
|
1015
|
+
for (const endpoint of normalized_api_endpoints) {
|
|
1016
|
+
if (!endpoint || typeof endpoint.handler !== 'function') {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const endpoint_route = endpoint.path || endpoint.name;
|
|
1021
|
+
if (typeof endpoint_route !== 'string' || endpoint_route.length === 0) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const publish_meta = { method: endpoint.method };
|
|
1026
|
+
if (endpoint.summary) publish_meta.summary = endpoint.summary;
|
|
1027
|
+
if (endpoint.description) publish_meta.description = endpoint.description;
|
|
1028
|
+
if (endpoint.tags) publish_meta.tags = endpoint.tags;
|
|
1029
|
+
if (endpoint.params) publish_meta.params = endpoint.params;
|
|
1030
|
+
if (endpoint.returns) publish_meta.returns = endpoint.returns;
|
|
1031
|
+
if (endpoint.deprecated) publish_meta.deprecated = endpoint.deprecated;
|
|
1032
|
+
if (endpoint.operationId) publish_meta.operationId = endpoint.operationId;
|
|
1033
|
+
if (endpoint.raw) publish_meta.raw = endpoint.raw;
|
|
1034
|
+
server_instance.publish(endpoint.path || endpoint.name, endpoint.handler, publish_meta);
|
|
1035
|
+
}
|
|
1036
|
+
// ── Data query endpoints ──────────────────────────────
|
|
1037
|
+
// `data` accepts an object map of name → {query_fn, adapter, schema}.
|
|
1038
|
+
// Each entry creates a Query_Resource + Query_Publisher at /api/data/<name>.
|
|
1039
|
+
let data_endpoint_count = 0;
|
|
1040
|
+
if (serve_options.data && typeof serve_options.data === 'object') {
|
|
1041
|
+
const Query_Publisher = require('./publishers/query-publisher');
|
|
1042
|
+
const Query_Resource = require('./resources/query-resource');
|
|
1043
|
+
const Array_Adapter = require('./resources/adapters/array-adapter');
|
|
1044
|
+
|
|
1045
|
+
for (const [data_name, data_spec] of Object.entries(serve_options.data)) {
|
|
1046
|
+
if (!data_spec) continue;
|
|
1047
|
+
|
|
1048
|
+
let query_fn;
|
|
1049
|
+
let resource = null;
|
|
1050
|
+
|
|
1051
|
+
if (typeof data_spec.query_fn === 'function') {
|
|
1052
|
+
query_fn = data_spec.query_fn;
|
|
1053
|
+
} else if (data_spec.adapter && typeof data_spec.adapter.query === 'function') {
|
|
1054
|
+
resource = new Query_Resource({
|
|
1055
|
+
name: data_name,
|
|
1056
|
+
adapter: data_spec.adapter,
|
|
1057
|
+
schema: data_spec.schema
|
|
1058
|
+
});
|
|
1059
|
+
query_fn = (params) => resource.query(params);
|
|
1060
|
+
} else if (Array.isArray(data_spec.data)) {
|
|
1061
|
+
const adapter = new Array_Adapter({ data: data_spec.data });
|
|
1062
|
+
resource = new Query_Resource({
|
|
1063
|
+
name: data_name,
|
|
1064
|
+
adapter,
|
|
1065
|
+
schema: data_spec.schema
|
|
1066
|
+
});
|
|
1067
|
+
query_fn = (params) => resource.query(params);
|
|
1068
|
+
} else {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (resource) {
|
|
1073
|
+
configured_resources.push(resource);
|
|
1074
|
+
if (!server_instance.configured_resources) {
|
|
1075
|
+
server_instance.configured_resources = configured_resources;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const data_route = ensure_route_leading_slash(`/api/data/${data_name}`);
|
|
1080
|
+
const publisher = new Query_Publisher({
|
|
1081
|
+
name: data_name,
|
|
1082
|
+
query_fn,
|
|
1083
|
+
schema: data_spec.schema
|
|
1084
|
+
});
|
|
1085
|
+
server_instance.server_router.set_route(data_route, publisher, publisher.handle_http);
|
|
1086
|
+
data_endpoint_count++;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// ── Swagger / OpenAPI auto-registration ──────────────
|
|
1091
|
+
// swagger: true → always enable
|
|
1092
|
+
// swagger: false → always disable
|
|
1093
|
+
// swagger: omitted → enable in non-production
|
|
1094
|
+
const swagger_option = serve_options.swagger;
|
|
1095
|
+
const swagger_enabled = swagger_option === true
|
|
1096
|
+
|| (swagger_option !== false && process.env.NODE_ENV !== 'production');
|
|
1097
|
+
|
|
1098
|
+
if (swagger_enabled) {
|
|
1099
|
+
const swagger_options = typeof swagger_option === 'object' ? swagger_option : {};
|
|
1100
|
+
server_instance._register_swagger_routes({
|
|
1101
|
+
title: swagger_options.title || serve_options.name,
|
|
1102
|
+
version: swagger_options.version,
|
|
1103
|
+
description: swagger_options.description
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const should_force_ready = !serve_options.ctrl && (
|
|
1108
|
+
normalized_api_endpoints.length > 0
|
|
1109
|
+
|| data_endpoint_count > 0
|
|
1110
|
+
|| use_manual_page_publication
|
|
1111
|
+
);
|
|
1112
|
+
|
|
410
1113
|
return new Promise((resolve, reject) => {
|
|
411
1114
|
let has_started = false;
|
|
412
1115
|
let has_settled = false;
|
|
@@ -448,6 +1151,13 @@ module.exports = (Server) => {
|
|
|
448
1151
|
|
|
449
1152
|
server_instance.port = actual_port;
|
|
450
1153
|
|
|
1154
|
+
const start_options = {
|
|
1155
|
+
...(serve_options.start && typeof serve_options.start === 'object' ? serve_options.start : {})
|
|
1156
|
+
};
|
|
1157
|
+
if (typeof serve_options.on_port_conflict === 'string' && !start_options.on_port_conflict) {
|
|
1158
|
+
start_options.on_port_conflict = serve_options.on_port_conflict;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
451
1161
|
server_instance.start(actual_port, (error) => {
|
|
452
1162
|
if (error) {
|
|
453
1163
|
return settle(reject, error);
|
|
@@ -458,7 +1168,7 @@ module.exports = (Server) => {
|
|
|
458
1168
|
}).catch((resource_error) => {
|
|
459
1169
|
settle(reject, resource_error);
|
|
460
1170
|
});
|
|
461
|
-
});
|
|
1171
|
+
}, start_options);
|
|
462
1172
|
};
|
|
463
1173
|
|
|
464
1174
|
server_instance.on('ready', () => {
|
|
@@ -476,7 +1186,7 @@ module.exports = (Server) => {
|
|
|
476
1186
|
});
|
|
477
1187
|
});
|
|
478
1188
|
|
|
479
|
-
if (
|
|
1189
|
+
if (should_force_ready) {
|
|
480
1190
|
server_instance.raise('ready');
|
|
481
1191
|
}
|
|
482
1192
|
|