jsgui3-server 0.0.147 → 0.0.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.github/workflows/control-scan-manifest-check.yml +31 -0
  2. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +40 -0
  3. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +39 -0
  4. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +39 -0
  5. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +39 -0
  6. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +39 -0
  7. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +40 -0
  8. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +39 -0
  9. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +40 -0
  10. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +40 -0
  11. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +39 -0
  12. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +39 -0
  13. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +44 -0
  14. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +45 -0
  15. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +39 -0
  16. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +39 -0
  17. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +42 -0
  18. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +40 -0
  19. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +43 -0
  20. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +40 -0
  21. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +40 -0
  22. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +40 -0
  23. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +39 -0
  24. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +39 -0
  25. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +39 -0
  26. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +39 -0
  27. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +39 -0
  28. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +41 -0
  29. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +44 -0
  30. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +40 -0
  31. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +40 -0
  32. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +39 -0
  33. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +39 -0
  34. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +39 -0
  35. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +39 -0
  36. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f9dee4ec18a96e09bee06bae.js +39 -0
  37. package/README.md +85 -3
  38. package/admin-ui/client.js +213 -0
  39. package/admin-ui/server.js +104 -0
  40. package/client/controls/auto-observable.js +207 -0
  41. package/dev-status.svg +139 -0
  42. package/docs/api-reference.md +301 -43
  43. package/docs/books/admin-ui/01-introduction.md +32 -0
  44. package/docs/books/admin-ui/02-architecture.md +92 -0
  45. package/docs/books/admin-ui/03-controls.md +194 -0
  46. package/docs/books/admin-ui/04-implementation-plan.md +62 -0
  47. package/docs/books/admin-ui/README.md +26 -0
  48. package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
  49. package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
  50. package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
  51. package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
  52. package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
  53. package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
  54. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
  55. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
  56. package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
  57. package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
  58. package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
  59. package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
  60. package/docs/bundling-system-deep-dive.md +9 -4
  61. package/docs/comprehensive-documentation.md +49 -18
  62. package/docs/configuration-reference.md +152 -27
  63. package/docs/core/README.md +19 -0
  64. package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
  65. package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
  66. package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
  67. package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
  68. package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
  69. package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
  70. package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
  71. package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
  72. package/docs/publishers-guide.md +59 -4
  73. package/docs/resources-guide.md +184 -35
  74. package/docs/simple-server-api-design.md +72 -17
  75. package/docs/system-architecture.md +18 -14
  76. package/examples/controls/15) window, observable SSE/server.js +6 -1
  77. package/examples/controls/19) window, auto observable ui/client.js +125 -0
  78. package/examples/controls/19) window, auto observable ui/server.js +73 -0
  79. package/examples/controls/20) window, task manager app/README.md +133 -0
  80. package/examples/controls/20) window, task manager app/client.js +797 -0
  81. package/examples/controls/20) window, task manager app/server.js +178 -0
  82. package/examples/controls/6) window, color_palette/client.js +165 -68
  83. package/examples/controls/9) window, date picker/client.js +362 -76
  84. package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
  85. package/examples/jsgui3-html/06) theming/client.js +22 -1
  86. package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
  87. package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
  88. package/lab/experiments/capture-color-controls.js +196 -0
  89. package/lab/results/screenshots/color-controls/full_page.png +0 -0
  90. package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
  91. package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
  92. package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
  93. package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
  94. package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
  95. package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
  96. package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
  97. package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
  98. package/lab/screenshot-utils.js +248 -0
  99. package/module.js +11 -4
  100. package/package.json +14 -4
  101. package/publishers/Publishers.js +4 -3
  102. package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
  103. package/publishers/http-observable-publisher.js +8 -0
  104. package/publishers/http-sse-publisher.js +341 -0
  105. package/publishers/http-webpage-publisher.js +13 -3
  106. package/publishers/http-webpageorsite-publisher.js +18 -0
  107. package/resources/process-resource.js +950 -0
  108. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +164 -46
  109. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
  110. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
  111. package/resources/remote-process-resource.js +355 -0
  112. package/resources/server-resource-pool.js +354 -41
  113. package/serve-factory.js +441 -259
  114. package/server.js +161 -16
  115. package/tests/README.md +66 -4
  116. package/tests/admin-ui-render.test.js +24 -0
  117. package/tests/assigners.test.js +56 -40
  118. package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
  119. package/tests/configuration-validation.test.js +21 -18
  120. package/tests/content-analysis.test.js +7 -6
  121. package/tests/control-optimizer-cache-behavior.test.js +52 -0
  122. package/tests/control-scan-manifest-regression.test.js +144 -0
  123. package/tests/end-to-end.test.js +15 -14
  124. package/tests/error-handling.test.js +222 -179
  125. package/tests/fixtures/bundling-default-button-client.js +37 -0
  126. package/tests/fixtures/bundling-default-window-client.js +34 -0
  127. package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
  128. package/tests/fixtures/resource-monitor-client.js +319 -0
  129. package/tests/helpers/puppeteer-e2e-harness.js +317 -0
  130. package/tests/http-sse-publisher.test.js +136 -0
  131. package/tests/performance.test.js +69 -65
  132. package/tests/process-resource.test.js +138 -0
  133. package/tests/publishers.test.js +7 -7
  134. package/tests/remote-process-resource.test.js +160 -0
  135. package/tests/sass-controls.e2e.test.js +7 -1
  136. package/tests/serve-resources.test.js +270 -0
  137. package/tests/serve.test.js +120 -50
  138. package/tests/server-resource-pool.test.js +106 -0
  139. package/tests/small-controls-bundle-size.test.js +252 -0
  140. package/tests/test-runner.js +13 -1
  141. package/tests/window-examples.puppeteer.test.js +204 -1
  142. package/tests/window-resource-integration.puppeteer.test.js +585 -0
  143. package/tests/temp_invalid.js +0 -7
  144. package/tests/temp_invalid_utf8.js +0 -1
  145. package/tests/temp_malformed.js +0 -10
package/serve-factory.js CHANGED
@@ -1,194 +1,366 @@
1
- const {
2
- truthy,
3
- guess_caller_file,
4
- find_default_client_path,
5
- load_default_control_from_client,
6
- ensure_route_leading_slash
7
- } = require('./serve-helpers');
8
- const lib_path = require('path');
9
- const Webpage = require('./website/webpage');
10
- const HTTP_Webpage_Publisher = require('./publishers/http-webpage-publisher');
11
- const Static_Route_HTTP_Responder = require('./http/responders/static/Static_Route_HTTP_Responder');
12
- const { get_port_or_free } = require('./port-utils');
13
-
14
-
1
+ const {
2
+ truthy,
3
+ guess_caller_file,
4
+ find_default_client_path,
5
+ load_default_control_from_client,
6
+ ensure_route_leading_slash
7
+ } = require('./serve-helpers');
8
+ const lib_path = require('path');
9
+ const Webpage = require('./website/webpage');
10
+ const HTTP_Webpage_Publisher = require('./publishers/http-webpage-publisher');
11
+ const HTTP_SSE_Publisher = require('./publishers/http-sse-publisher');
12
+ const Static_Route_HTTP_Responder = require('./http/responders/static/Static_Route_HTTP_Responder');
13
+ const Process_Resource = require('./resources/process-resource');
14
+ const Remote_Process_Resource = require('./resources/remote-process-resource');
15
+ const { get_port_or_free } = require('./port-utils');
16
+
15
17
  const prepare_webpage_route = (server, route, page_options = {}, defaults = {}) => {
16
- return new Promise((resolve, reject) => {
17
- try {
18
- const {
19
- title,
20
- name,
21
- description
22
- } = page_options;
23
- const content_ctrl = page_options.content || page_options.ctrl || page_options.Ctrl;
24
- if (typeof content_ctrl !== 'function') {
25
- return reject(new Error(`Page at route "${route}" requires a control constructor as content.`));
26
- }
27
- const caller_dir = defaults.caller_dir || process.cwd();
28
- const explicit_client_path = page_options.clientPath || page_options.src_path_client_js || page_options.disk_path_client_js;
29
- const guessed_client_path = find_default_client_path(explicit_client_path, caller_dir);
30
- const webpage = new Webpage({
31
- name: name || title || `Page ${route}`,
32
- title: title,
33
- content: content_ctrl,
34
- path: route
35
- });
36
-
37
- const publisher_options = {
38
- webpage
39
- };
40
- if (guessed_client_path) publisher_options.src_path_client_js = guessed_client_path;
41
- if (truthy(defaults.debug)) publisher_options.debug = true;
42
- if (defaults.style !== undefined) publisher_options.style = defaults.style;
43
- if (defaults.bundler !== undefined) publisher_options.bundler = defaults.bundler;
44
-
45
- const webpage_publisher = new HTTP_Webpage_Publisher(publisher_options);
46
- webpage_publisher.on('ready', (bundle) => {
47
- try {
48
- if (bundle && bundle._arr) {
49
- for (const item of bundle._arr) {
50
- const static_responder = new Static_Route_HTTP_Responder(item);
51
- server.router.set_route(item.route, static_responder, static_responder.handle_http);
52
- }
53
- resolve();
54
- return;
55
- }
56
- reject(new Error(`Unexpected bundle format when preparing route ${route}`));
57
- } catch (err) {
58
- reject(err);
59
- }
60
- });
61
- webpage_publisher.on('error', reject);
62
- } catch (err) {
63
- reject(err);
64
- }
65
- });
66
- };
67
-
68
-
69
- module.exports = (Server) => {
70
- const serve = function(input, maybe_options, maybe_callback) {
71
- let callback = null;
72
- if (typeof maybe_options === 'function') {
73
- callback = maybe_options;
74
- maybe_options = undefined;
75
- } else if (typeof maybe_callback === 'function') {
76
- callback = maybe_callback;
77
- }
78
-
79
- let serve_options = {};
80
- if (typeof input === 'function') {
81
- serve_options.ctrl = input;
82
- } else if (input && typeof input === 'object') {
83
- serve_options = { ...input
84
- };
85
- } else if (input !== undefined) {
86
- throw new Error('First argument to Server.serve must be a control constructor or options object.');
87
- }
88
- if (maybe_options && typeof maybe_options === 'object') {
89
- serve_options = { ...serve_options,
90
- ...maybe_options
91
- };
92
- }
93
-
94
- const caller_file = serve_options.caller_file || serve_options.callerFile || guess_caller_file();
95
- const caller_dir = serve_options.root ? lib_path.resolve(process.cwd(), serve_options.root) : (caller_file ? lib_path.dirname(caller_file) : process.cwd());
96
-
97
- if (!serve_options.ctrl && serve_options.Ctrl) serve_options.ctrl = serve_options.Ctrl;
98
- if (!serve_options.ctrl && serve_options.page) {
99
- const page_config = serve_options.page;
100
- serve_options.ctrl = page_config.content || page_config.ctrl || page_config.Ctrl;
101
- serve_options.page_route = ensure_route_leading_slash(page_config.route || '/');
102
- serve_options.page_config = page_config;
103
- }
104
-
105
- let additional_pages = [];
106
- if (!serve_options.ctrl && serve_options.pages && typeof serve_options.pages === 'object') {
107
- const page_entries = Object.entries(serve_options.pages);
108
- if (!page_entries.length) {
109
- throw new Error('`pages` option requires at least one entry.');
110
- }
111
- const normalized_pages = page_entries.map(([route, cfg]) => [ensure_route_leading_slash(route), cfg || {}]);
112
- const root_entry = normalized_pages.find(([route]) => route === '/') || normalized_pages[0];
113
- serve_options.ctrl = (root_entry[1].content || root_entry[1].ctrl || root_entry[1].Ctrl);
114
- serve_options.page_route = root_entry[0];
115
- serve_options.page_config = root_entry[1];
116
- additional_pages = normalized_pages.filter(([route]) => route !== serve_options.page_route);
117
- }
118
-
119
- const explicit_client_path = serve_options.clientPath || serve_options.client_path || serve_options.src_path_client_js || serve_options.disk_path_client_js;
120
- const root_client_path = find_default_client_path(explicit_client_path, caller_dir);
121
-
122
- if (typeof serve_options.ctrl !== 'function' && root_client_path) {
123
- const auto_ctrl = load_default_control_from_client(root_client_path);
124
- if (typeof auto_ctrl === 'function') {
125
- serve_options.ctrl = auto_ctrl;
126
- }
127
- }
128
-
129
- if (serve_options.page_config && typeof serve_options.ctrl !== 'function') {
130
- throw new Error('`page` option requires a control constructor.');
131
- }
132
- if (additional_pages.length && typeof serve_options.ctrl !== 'function') {
133
- throw new Error('`pages` option requires at least one control constructor.');
134
- }
135
-
136
- const port = Number.isFinite(serve_options.port) ? Number(serve_options.port) : (serve_options.port === 'auto' ? 0 : (process.env.PORT ? Number(process.env.PORT) : 8080));
137
- const auto_port = serve_options.autoPort !== false && (port === 0 || serve_options.port === 'auto' || serve_options.autoPort === true);
138
-
139
- if (!Number.isFinite(port) && serve_options.port !== 'auto') {
140
- throw new Error('Invalid port specified for Server.serve');
141
- }
142
-
18
+ return new Promise((resolve, reject) => {
19
+ try {
20
+ const {
21
+ title,
22
+ name
23
+ } = page_options;
24
+ const content_ctrl = page_options.content || page_options.ctrl || page_options.Ctrl;
25
+ if (typeof content_ctrl !== 'function') {
26
+ return reject(new Error(`Page at route "${route}" requires a control constructor as content.`));
27
+ }
28
+
29
+ const caller_dir = defaults.caller_dir || process.cwd();
30
+ const explicit_client_path = page_options.clientPath || page_options.src_path_client_js || page_options.disk_path_client_js;
31
+ const guessed_client_path = find_default_client_path(explicit_client_path, caller_dir);
32
+
33
+ const webpage = new Webpage({
34
+ name: name || title || `Page ${route}`,
35
+ title,
36
+ content: content_ctrl,
37
+ path: route
38
+ });
39
+
40
+ const publisher_options = {
41
+ webpage
42
+ };
43
+ if (guessed_client_path) publisher_options.src_path_client_js = guessed_client_path;
44
+ if (truthy(defaults.debug)) publisher_options.debug = true;
45
+ if (defaults.style !== undefined) publisher_options.style = defaults.style;
46
+ if (defaults.bundler !== undefined) publisher_options.bundler = defaults.bundler;
47
+
48
+ const webpage_publisher = new HTTP_Webpage_Publisher(publisher_options);
49
+ webpage_publisher.on('ready', (bundle) => {
50
+ try {
51
+ if (bundle && bundle._arr) {
52
+ for (const item of bundle._arr) {
53
+ const static_responder = new Static_Route_HTTP_Responder(item);
54
+ server.router.set_route(item.route, static_responder, static_responder.handle_http);
55
+ }
56
+ resolve();
57
+ return;
58
+ }
59
+ reject(new Error(`Unexpected bundle format when preparing route ${route}`));
60
+ } catch (error) {
61
+ reject(error);
62
+ }
63
+ });
64
+ webpage_publisher.on('error', reject);
65
+ } catch (error) {
66
+ reject(error);
67
+ }
68
+ });
69
+ };
70
+
71
+ const invoke_resource_method = (resource, method_name) => {
72
+ if (!resource || typeof resource[method_name] !== 'function') {
73
+ return Promise.resolve(true);
74
+ }
75
+
76
+ if (resource[method_name].length >= 1) {
77
+ return new Promise((resolve, reject) => {
78
+ resource[method_name]((error, result) => {
79
+ if (error) {
80
+ reject(error);
81
+ return;
82
+ }
83
+ resolve(result);
84
+ });
85
+ });
86
+ }
87
+
88
+ const method_result = resource[method_name]();
89
+ if (method_result && typeof method_result.then === 'function') {
90
+ return method_result;
91
+ }
92
+
93
+ return Promise.resolve(method_result);
94
+ };
95
+
96
+ const is_resource_like = (resource_value) => {
97
+ return !!resource_value
98
+ && typeof resource_value === 'object'
99
+ && (typeof resource_value.start === 'function'
100
+ || typeof resource_value.meets_requirements === 'function'
101
+ || typeof resource_value.get_abstract === 'function');
102
+ };
103
+
104
+ const instantiate_custom_resource = (resource_name, resource_spec, resource_constructor) => {
105
+ const constructor_spec = resource_spec && typeof resource_spec.spec === 'object'
106
+ ? {
107
+ ...resource_spec.spec
108
+ }
109
+ : {
110
+ ...resource_spec
111
+ };
112
+
113
+ if (!constructor_spec.name) {
114
+ constructor_spec.name = resource_name;
115
+ }
116
+
117
+ delete constructor_spec.type;
118
+ delete constructor_spec.class;
119
+ delete constructor_spec.Ctor;
120
+ delete constructor_spec.constructor_fn;
121
+ delete constructor_spec.instance;
122
+ delete constructor_spec.resource;
123
+ delete constructor_spec.spec;
124
+
125
+ return new resource_constructor(constructor_spec);
126
+ };
127
+
128
+ const create_configured_resource = (resource_name, resource_spec = {}) => {
129
+ if (is_resource_like(resource_spec)) {
130
+ if (!resource_spec.name) {
131
+ resource_spec.name = resource_name;
132
+ }
133
+ return resource_spec;
134
+ }
135
+
136
+ if (typeof resource_spec === 'function') {
137
+ return instantiate_custom_resource(resource_name, {}, resource_spec);
138
+ }
139
+
140
+ const normalized_spec = {
141
+ ...resource_spec,
142
+ name: resource_spec.name || resource_name
143
+ };
144
+
145
+ const explicit_instance = normalized_spec.instance || normalized_spec.resource;
146
+ if (is_resource_like(explicit_instance)) {
147
+ if (!explicit_instance.name) {
148
+ explicit_instance.name = normalized_spec.name;
149
+ }
150
+ return explicit_instance;
151
+ }
152
+
153
+ const resource_constructor = normalized_spec.class || normalized_spec.Ctor || normalized_spec.constructor_fn;
154
+ if (typeof resource_constructor === 'function') {
155
+ return instantiate_custom_resource(resource_name, normalized_spec, resource_constructor);
156
+ }
157
+
158
+ const resource_type = String(normalized_spec.type || 'process').toLowerCase();
159
+ if (resource_type === 'process' || resource_type === 'local') {
160
+ return new Process_Resource(normalized_spec);
161
+ }
162
+ if (resource_type === 'remote' || resource_type === 'http') {
163
+ return new Remote_Process_Resource(normalized_spec);
164
+ }
165
+ if (resource_type === 'resource' || resource_type === 'in_process' || resource_type === 'in-process') {
166
+ throw new Error(
167
+ `resources.${resource_name} requires one of: { instance }, { resource }, { class }, { constructor_fn }, or a resource object directly.`
168
+ );
169
+ }
170
+
171
+ if (normalized_spec.command || normalized_spec.processManager) {
172
+ return new Process_Resource(normalized_spec);
173
+ }
174
+ if (normalized_spec.host || normalized_spec.endpoints) {
175
+ return new Remote_Process_Resource(normalized_spec);
176
+ }
177
+
178
+ throw new Error(`Unsupported resources.${resource_name}.type: ${normalized_spec.type}`);
179
+ };
180
+
181
+ const normalize_resource_entries = (resources_option) => {
182
+ if (!resources_option) {
183
+ return [];
184
+ }
185
+
186
+ if (Array.isArray(resources_option)) {
187
+ return resources_option.map((resource_spec, index) => {
188
+ const inferred_name = (resource_spec && resource_spec.name) || `resource_${index + 1}`;
189
+ return [inferred_name, resource_spec || {}];
190
+ });
191
+ }
192
+
193
+ if (typeof resources_option === 'object') {
194
+ return Object.entries(resources_option);
195
+ }
196
+
197
+ throw new Error('`resources` must be an object map or an array.');
198
+ };
199
+
200
+ module.exports = (Server) => {
201
+ const serve = function(input, maybe_options, maybe_callback) {
202
+ let callback = null;
203
+ if (typeof maybe_options === 'function') {
204
+ callback = maybe_options;
205
+ maybe_options = undefined;
206
+ } else if (typeof maybe_callback === 'function') {
207
+ callback = maybe_callback;
208
+ }
209
+
210
+ let serve_options = {};
211
+ if (typeof input === 'function') {
212
+ serve_options.ctrl = input;
213
+ } else if (input && typeof input === 'object') {
214
+ serve_options = {
215
+ ...input
216
+ };
217
+ } else if (input !== undefined) {
218
+ throw new Error('First argument to Server.serve must be a control constructor or options object.');
219
+ }
220
+
221
+ if (maybe_options && typeof maybe_options === 'object') {
222
+ serve_options = {
223
+ ...serve_options,
224
+ ...maybe_options
225
+ };
226
+ }
227
+
228
+ const caller_file = serve_options.caller_file || serve_options.callerFile || guess_caller_file();
229
+ const caller_dir = serve_options.root
230
+ ? lib_path.resolve(process.cwd(), serve_options.root)
231
+ : (caller_file ? lib_path.dirname(caller_file) : process.cwd());
232
+
233
+ if (!serve_options.ctrl && serve_options.Ctrl) serve_options.ctrl = serve_options.Ctrl;
234
+ if (!serve_options.ctrl && serve_options.page) {
235
+ const page_config = serve_options.page;
236
+ serve_options.ctrl = page_config.content || page_config.ctrl || page_config.Ctrl;
237
+ serve_options.page_route = ensure_route_leading_slash(page_config.route || '/');
238
+ serve_options.page_config = page_config;
239
+ }
240
+
241
+ let additional_pages = [];
242
+ if (!serve_options.ctrl && serve_options.pages && typeof serve_options.pages === 'object') {
243
+ const page_entries = Object.entries(serve_options.pages);
244
+ if (!page_entries.length) {
245
+ throw new Error('`pages` option requires at least one entry.');
246
+ }
247
+
248
+ const normalized_pages = page_entries.map(([route, cfg]) => [ensure_route_leading_slash(route), cfg || {}]);
249
+ const root_entry = normalized_pages.find(([route]) => route === '/') || normalized_pages[0];
250
+ serve_options.ctrl = (root_entry[1].content || root_entry[1].ctrl || root_entry[1].Ctrl);
251
+ serve_options.page_route = root_entry[0];
252
+ serve_options.page_config = root_entry[1];
253
+ additional_pages = normalized_pages.filter(([route]) => route !== serve_options.page_route);
254
+ }
255
+
256
+ const explicit_client_path = serve_options.clientPath || serve_options.client_path || serve_options.src_path_client_js || serve_options.disk_path_client_js;
257
+ const root_client_path = find_default_client_path(explicit_client_path, caller_dir);
258
+
259
+ if (typeof serve_options.ctrl !== 'function' && root_client_path) {
260
+ const auto_ctrl = load_default_control_from_client(root_client_path);
261
+ if (typeof auto_ctrl === 'function') {
262
+ serve_options.ctrl = auto_ctrl;
263
+ }
264
+ }
265
+
266
+ if (serve_options.page_config && typeof serve_options.ctrl !== 'function') {
267
+ throw new Error('`page` option requires a control constructor.');
268
+ }
269
+ if (additional_pages.length && typeof serve_options.ctrl !== 'function') {
270
+ throw new Error('`pages` option requires at least one control constructor.');
271
+ }
272
+
273
+ const port = Number.isFinite(serve_options.port)
274
+ ? Number(serve_options.port)
275
+ : (serve_options.port === 'auto' ? 0 : (process.env.PORT ? Number(process.env.PORT) : 8080));
276
+ const auto_port = serve_options.autoPort !== false && (port === 0 || serve_options.port === 'auto' || serve_options.autoPort === true);
277
+ if (!Number.isFinite(port) && serve_options.port !== 'auto') {
278
+ throw new Error('Invalid port specified for Server.serve');
279
+ }
280
+
143
281
  const host = serve_options.host || process.env.HOST || null;
144
282
  const debug_enabled = serve_options.debug !== undefined ? truthy(serve_options.debug) : truthy(process.env.JSGUI_DEBUG);
145
283
  const style_config = serve_options.style;
146
284
  const bundler_config = serve_options.bundler;
147
-
285
+
148
286
  const server_spec = {
149
287
  name: serve_options.name || 'jsgui3 server',
150
288
  debug: debug_enabled
151
289
  };
152
- if (style_config !== undefined) {
153
- server_spec.style = style_config;
290
+ if (style_config !== undefined) server_spec.style = style_config;
291
+ if (bundler_config !== undefined) server_spec.bundler = bundler_config;
292
+
293
+ if (typeof serve_options.ctrl === 'function') {
294
+ server_spec.Ctrl = serve_options.ctrl;
295
+ } else if (serve_options.api && typeof serve_options.api === 'object') {
296
+ server_spec.website = false;
297
+ }
298
+
299
+ if (root_client_path) {
300
+ server_spec.src_path_client_js = root_client_path;
154
301
  }
155
- if (bundler_config !== undefined) {
156
- server_spec.bundler = bundler_config;
302
+
303
+ const server_instance = new Server(server_spec);
304
+ if (host) {
305
+ server_instance.allowed_addresses = Array.isArray(host) ? host : [host];
157
306
  }
158
- if (typeof serve_options.ctrl === 'function') {
159
- server_spec.Ctrl = serve_options.ctrl;
160
- } else if (serve_options.api && typeof serve_options.api === 'object') {
161
- // API-only server: explicitly disable website setup
162
- server_spec.website = false;
163
- }
164
- if (root_client_path) {
165
- server_spec.src_path_client_js = root_client_path;
166
- }
167
-
168
- const server_instance = new Server(server_spec);
169
- if (host) {
170
- server_instance.allowed_addresses = Array.isArray(host) ? host : [host];
171
- }
172
-
173
- const settle = (resolver, value) => {
174
- if (callback) {
175
- try {
176
- callback(resolver === reject ? value : null, resolver === resolve ? value : undefined);
177
- } catch (err) {
178
- // Swallow callback errors after resolution
179
- }
180
- }
181
- return resolver(value);
182
- };
183
-
184
- let has_started = false;
185
-
307
+
308
+ const configured_resources = [];
309
+ const resource_entries = normalize_resource_entries(serve_options.resources);
310
+ if (resource_entries.length) {
311
+ for (const [resource_name, resource_spec] of resource_entries) {
312
+ configured_resources.push(create_configured_resource(resource_name, resource_spec || {}));
313
+ }
314
+ server_instance.configured_resources = configured_resources;
315
+ }
316
+
317
+ if (serve_options.events) {
318
+ const event_options = typeof serve_options.events === 'object' ? serve_options.events : {};
319
+ const events_route = ensure_route_leading_slash(event_options.route || '/events');
320
+ const sse_publisher = new HTTP_SSE_Publisher({
321
+ name: event_options.name || 'events',
322
+ ...event_options
323
+ });
324
+
325
+ server_instance.sse_publisher = sse_publisher;
326
+ server_instance.server_router.set_route(events_route, sse_publisher, sse_publisher.handle_http);
327
+
328
+ const pool_event_names = ['resource_state_change', 'crashed', 'unhealthy', 'unreachable', 'recovered'];
329
+ pool_event_names.forEach((event_name) => {
330
+ server_instance.resource_pool.on(event_name, (event_data) => {
331
+ sse_publisher.broadcast(event_name, event_data);
332
+ });
333
+ });
334
+ }
335
+
336
+ const attach_configured_resources = () => {
337
+ for (const resource of configured_resources) {
338
+ if (!server_instance.resource_pool.has_resource(resource.name)) {
339
+ server_instance.resource_pool.add(resource);
340
+ }
341
+ }
342
+ };
343
+
344
+ const start_configured_resources = async () => {
345
+ if (!configured_resources.length) {
346
+ return;
347
+ }
348
+
349
+ attach_configured_resources();
350
+ await Promise.all(configured_resources.map((resource) => invoke_resource_method(resource, 'start')));
351
+ };
352
+
353
+ const startup_ready_timeout_ms = Number.isFinite(serve_options.readyTimeoutMs) && Number(serve_options.readyTimeoutMs) > 0
354
+ ? Number(serve_options.readyTimeoutMs)
355
+ : 120000;
356
+
186
357
  const extra_page_promises = additional_pages.map(([route, cfg]) => prepare_webpage_route(server_instance, route, cfg, {
187
358
  caller_dir,
188
359
  debug: debug_enabled,
189
360
  style: style_config,
190
361
  bundler: bundler_config
191
362
  }));
363
+
192
364
  if (serve_options.page_config && serve_options.page_route && serve_options.page_route !== '/') {
193
365
  extra_page_promises.unshift(prepare_webpage_route(server_instance, serve_options.page_route, serve_options.page_config, {
194
366
  caller_dir,
@@ -197,88 +369,98 @@ module.exports = (Server) => {
197
369
  bundler: bundler_config
198
370
  }));
199
371
  }
200
-
201
- if (serve_options.api && typeof serve_options.api === 'object') {
202
- console.log('🔍 DEBUG: Setting up API routes');
203
- for (const [name, handler] of Object.entries(serve_options.api)) {
204
- if (typeof handler === 'function') {
205
- console.log(`🔍 DEBUG: Publishing API route: ${name}`);
206
- server_instance.publish(name, handler);
207
- }
208
- }
209
- console.log('🔍 DEBUG: API routes setup complete');
210
- }
211
-
212
- return new Promise((resolve, reject) => {
213
- const start_server = async () => {
214
- if (has_started) return;
215
- has_started = true;
216
-
217
- // Determine the actual port to use
218
- let actual_port = port;
219
- if (auto_port) {
220
- try {
221
- const check_host = host || '127.0.0.1';
222
- actual_port = await get_port_or_free(port, Array.isArray(check_host) ? check_host[0] : check_host);
223
- if (actual_port !== port && port !== 0) {
224
- console.log(`Port ${port} was in use, using port ${actual_port} instead`);
225
- }
226
- } catch (err) {
227
- console.error('Failed to find free port:', err);
228
- return settle(reject, err);
229
- }
230
- }
231
-
232
- // Store the actual port on the server instance for reference
233
- server_instance.port = actual_port;
234
-
235
- console.log('🔍 DEBUG: Calling server_instance.start()');
236
- server_instance.start(actual_port, (err) => {
237
- if (err) {
238
- console.log('🔍 DEBUG: server_instance.start() failed:', err);
239
- return settle(reject, err);
240
- }
241
- console.log('🔍 DEBUG: server_instance.start() succeeded');
242
- const message = host ? `Serving on http://${Array.isArray(host) ? host[0] : host}:${actual_port}/` : `Serving on port ${actual_port} (all IPv4 interfaces)`;
243
- console.log(message);
244
- console.log('Server ready');
245
- settle(resolve, server_instance);
246
- });
247
- };
248
-
249
- console.log('🔍 DEBUG: Setting up ready event listener');
250
- server_instance.on('ready', () => {
251
- console.log('🔍 DEBUG: Ready event fired');
252
- Promise.allSettled(extra_page_promises).then(results => {
253
- const rejected_entry = results.find(result => result.status === 'rejected');
254
- if (rejected_entry) {
255
- console.log('🔍 DEBUG: Extra page promise rejected:', rejected_entry.reason);
256
- return settle(reject, rejected_entry.reason);
257
- }
258
- console.log('🔍 DEBUG: All extra page promises resolved, calling start_server()');
259
- start_server();
260
- }).catch(err => {
261
- console.log('🔍 DEBUG: Extra page promises error:', err);
262
- settle(reject, err);
263
- });
264
- });
265
-
266
- // For API-only servers, trigger ready immediately after API setup
267
- if (serve_options.api && typeof serve_options.api === 'object' && !serve_options.ctrl) {
268
- console.log('🔍 DEBUG: API-only server detected, triggering ready event');
269
- server_instance.raise('ready');
270
- }
271
-
272
- console.log('🔍 DEBUG: Setting up fallback timeout');
273
- setTimeout(() => {
274
- // Fallback in case ready event never fires (should not happen, but guard just in case)
275
- console.log('🔍 DEBUG: Fallback timeout triggered, has_started:', has_started);
276
- if (!has_started) {
277
- console.log('🔍 DEBUG: Calling start_server() from fallback');
278
- start_server();
279
- }
280
- }, 2000).unref?.();
281
- });
282
- };
283
- return serve;
284
- }
372
+
373
+ if (serve_options.api && typeof serve_options.api === 'object') {
374
+ for (const [name, handler] of Object.entries(serve_options.api)) {
375
+ if (typeof handler === 'function') {
376
+ server_instance.publish(name, handler);
377
+ }
378
+ }
379
+ }
380
+
381
+ return new Promise((resolve, reject) => {
382
+ let has_started = false;
383
+ let has_settled = false;
384
+ let ready_timeout_handle = null;
385
+
386
+ const settle = (resolver, value) => {
387
+ if (has_settled) {
388
+ return;
389
+ }
390
+ has_settled = true;
391
+ if (ready_timeout_handle) {
392
+ clearTimeout(ready_timeout_handle);
393
+ ready_timeout_handle = null;
394
+ }
395
+
396
+ if (callback) {
397
+ try {
398
+ callback(resolver === reject ? value : null, resolver === resolve ? value : undefined);
399
+ } catch {
400
+ // Ignore callback errors after settlement.
401
+ }
402
+ }
403
+ resolver(value);
404
+ };
405
+
406
+ const start_server = async () => {
407
+ if (has_started || has_settled) return;
408
+ has_started = true;
409
+
410
+ let actual_port = port;
411
+ if (auto_port) {
412
+ try {
413
+ const check_host = host || '127.0.0.1';
414
+ actual_port = await get_port_or_free(port, Array.isArray(check_host) ? check_host[0] : check_host);
415
+ } catch (error) {
416
+ return settle(reject, error);
417
+ }
418
+ }
419
+
420
+ server_instance.port = actual_port;
421
+
422
+ server_instance.start(actual_port, (error) => {
423
+ if (error) {
424
+ return settle(reject, error);
425
+ }
426
+
427
+ start_configured_resources().then(() => {
428
+ settle(resolve, server_instance);
429
+ }).catch((resource_error) => {
430
+ settle(reject, resource_error);
431
+ });
432
+ });
433
+ };
434
+
435
+ server_instance.on('ready', () => {
436
+ if (has_settled) {
437
+ return;
438
+ }
439
+ Promise.allSettled(extra_page_promises).then((results) => {
440
+ const rejected_entry = results.find((result) => result.status === 'rejected');
441
+ if (rejected_entry) {
442
+ return settle(reject, rejected_entry.reason);
443
+ }
444
+ start_server();
445
+ }).catch((error) => {
446
+ settle(reject, error);
447
+ });
448
+ });
449
+
450
+ if (serve_options.api && typeof serve_options.api === 'object' && !serve_options.ctrl) {
451
+ server_instance.raise('ready');
452
+ }
453
+
454
+ ready_timeout_handle = setTimeout(() => {
455
+ if (!has_settled && !has_started) {
456
+ settle(reject, new Error(
457
+ `Server.serve timed out waiting for "ready" before start (${startup_ready_timeout_ms}ms).`
458
+ ));
459
+ }
460
+ }, startup_ready_timeout_ms);
461
+ ready_timeout_handle.unref?.();
462
+ });
463
+ };
464
+
465
+ return serve;
466
+ };