jsgui3-server 0.0.140 → 0.0.141

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 (32) hide show
  1. package/.github/agents/jsgui3-server.agent.md +699 -0
  2. package/.github/instructions/copilot.instructions.md +180 -0
  3. package/.playwright-mcp/page-2025-11-29T23-39-18-629Z.png +0 -0
  4. package/.playwright-mcp/page-2025-11-29T23-39-31-903Z.png +0 -0
  5. package/.playwright-mcp/page-2025-11-30T00-33-56-265Z.png +0 -0
  6. package/.playwright-mcp/page-2025-11-30T00-34-06-619Z.png +0 -0
  7. package/docs/agent-development-guide.md +108 -4
  8. package/docs/api-reference.md +116 -0
  9. package/docs/controls-development.md +127 -0
  10. package/docs/css/luxuryObsidianCss.js +1203 -0
  11. package/docs/css/obsidian-scrollbars.css +370 -0
  12. package/docs/diagrams/jsgui3-stack.svg +568 -0
  13. package/docs/guides/JSGUI3_UI_ARCHITECTURE_GUIDE.md +2527 -0
  14. package/docs/guides/OBSIDIAN_LUXURY_DESIGN_GUIDE.md +847 -0
  15. package/docs/jsgui3-vs-express-comparison.svg +542 -0
  16. package/docs/jsgui3-vs-nestjs-comparison.svg +550 -0
  17. package/docs/publishers-guide.md +76 -0
  18. package/docs/troubleshooting.md +51 -0
  19. package/examples/controls/15) window, observable SSE/README.md +125 -0
  20. package/examples/controls/15) window, observable SSE/check.js +144 -0
  21. package/examples/controls/15) window, observable SSE/client.js +395 -0
  22. package/examples/controls/15) window, observable SSE/server.js +111 -0
  23. package/http/responders/static/Static_Route_HTTP_Responder.js +16 -16
  24. package/module.js +7 -0
  25. package/package.json +7 -6
  26. package/port-utils.js +112 -0
  27. package/serve-factory.js +27 -5
  28. package/tests/README.md +40 -26
  29. package/tests/examples-controls.e2e.test.js +164 -0
  30. package/tests/observable-sse.test.js +363 -0
  31. package/tests/port-utils.test.js +114 -0
  32. package/tests/test-runner.js +13 -12
package/port-utils.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Port utilities for jsgui3-server
3
+ * Provides automatic free port detection and port management
4
+ */
5
+
6
+ const net = require('net');
7
+
8
+ /**
9
+ * Find a free port on the specified host
10
+ * @param {Object} options - Options for port selection
11
+ * @param {string} [options.host='127.0.0.1'] - Host to check
12
+ * @param {number} [options.startPort=8080] - Starting port to try (0 = let OS choose)
13
+ * @param {number} [options.endPort=65535] - Maximum port to try
14
+ * @returns {Promise<number>} - A free port number
15
+ */
16
+ function get_free_port(options = {}) {
17
+ const host = options.host || '127.0.0.1';
18
+ const start_port = options.startPort || options.start_port || 0;
19
+
20
+ return new Promise((resolve, reject) => {
21
+ const server = net.createServer();
22
+
23
+ server.on('error', (err) => {
24
+ if (err.code === 'EADDRINUSE' && start_port > 0) {
25
+ // Try next port if specific port was requested
26
+ get_free_port({ ...options, startPort: start_port + 1 })
27
+ .then(resolve)
28
+ .catch(reject);
29
+ } else {
30
+ reject(err);
31
+ }
32
+ });
33
+
34
+ server.listen(start_port, host, () => {
35
+ const { port } = server.address();
36
+ server.close((err) => {
37
+ if (err) {
38
+ reject(err);
39
+ } else {
40
+ resolve(port);
41
+ }
42
+ });
43
+ });
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Check if a specific port is available
49
+ * @param {number} port - Port to check
50
+ * @param {string} [host='127.0.0.1'] - Host to check
51
+ * @returns {Promise<boolean>} - True if port is available
52
+ */
53
+ function is_port_available(port, host = '127.0.0.1') {
54
+ return new Promise((resolve) => {
55
+ const server = net.createServer();
56
+
57
+ server.on('error', () => {
58
+ resolve(false);
59
+ });
60
+
61
+ server.listen(port, host, () => {
62
+ server.close(() => {
63
+ resolve(true);
64
+ });
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Find multiple free ports
71
+ * @param {number} count - Number of ports to find
72
+ * @param {Object} options - Options for port selection
73
+ * @returns {Promise<number[]>} - Array of free port numbers
74
+ */
75
+ async function get_free_ports(count, options = {}) {
76
+ const ports = [];
77
+ for (let i = 0; i < count; i++) {
78
+ const port = await get_free_port(options);
79
+ ports.push(port);
80
+ }
81
+ return ports;
82
+ }
83
+
84
+ /**
85
+ * Get a free port, preferring a specific port if available
86
+ * @param {number} preferred_port - Preferred port (0 = auto-select)
87
+ * @param {string} [host='127.0.0.1'] - Host to check
88
+ * @returns {Promise<number>} - The preferred port if available, or a free port
89
+ */
90
+ async function get_port_or_free(preferred_port, host = '127.0.0.1') {
91
+ // If 0 or undefined, always auto-select
92
+ if (!preferred_port || preferred_port === 0) {
93
+ return get_free_port({ host });
94
+ }
95
+
96
+ // Check if preferred port is available
97
+ const available = await is_port_available(preferred_port, host);
98
+ if (available) {
99
+ return preferred_port;
100
+ }
101
+
102
+ // Fall back to auto-select
103
+ console.log(`Port ${preferred_port} is in use, selecting a free port...`);
104
+ return get_free_port({ host });
105
+ }
106
+
107
+ module.exports = {
108
+ get_free_port,
109
+ is_port_available,
110
+ get_free_ports,
111
+ get_port_or_free
112
+ };
package/serve-factory.js CHANGED
@@ -9,6 +9,7 @@ const lib_path = require('path');
9
9
  const Webpage = require('./website/webpage');
10
10
  const HTTP_Webpage_Publisher = require('./publishers/http-webpage-publisher');
11
11
  const Static_Route_HTTP_Responder = require('./http/responders/static/Static_Route_HTTP_Responder');
12
+ const { get_port_or_free } = require('./port-utils');
12
13
 
13
14
 
14
15
  const prepare_webpage_route = (server, route, page_options = {}, defaults = {}) => {
@@ -130,8 +131,10 @@ module.exports = (Server) => {
130
131
  throw new Error('`pages` option requires at least one control constructor.');
131
132
  }
132
133
 
133
- const port = Number.isFinite(serve_options.port) ? Number(serve_options.port) : (process.env.PORT ? Number(process.env.PORT) : 8080);
134
- if (!Number.isFinite(port)) {
134
+ const port = Number.isFinite(serve_options.port) ? Number(serve_options.port) : (serve_options.port === 'auto' ? 0 : (process.env.PORT ? Number(process.env.PORT) : 8080));
135
+ const auto_port = serve_options.autoPort !== false && (port === 0 || serve_options.port === 'auto' || serve_options.autoPort === true);
136
+
137
+ if (!Number.isFinite(port) && serve_options.port !== 'auto') {
135
138
  throw new Error('Invalid port specified for Server.serve');
136
139
  }
137
140
 
@@ -193,17 +196,36 @@ module.exports = (Server) => {
193
196
  }
194
197
 
195
198
  return new Promise((resolve, reject) => {
196
- const start_server = () => {
199
+ const start_server = async () => {
197
200
  if (has_started) return;
198
201
  has_started = true;
202
+
203
+ // Determine the actual port to use
204
+ let actual_port = port;
205
+ if (auto_port) {
206
+ try {
207
+ const check_host = host || '127.0.0.1';
208
+ actual_port = await get_port_or_free(port, Array.isArray(check_host) ? check_host[0] : check_host);
209
+ if (actual_port !== port && port !== 0) {
210
+ console.log(`Port ${port} was in use, using port ${actual_port} instead`);
211
+ }
212
+ } catch (err) {
213
+ console.error('Failed to find free port:', err);
214
+ return settle(reject, err);
215
+ }
216
+ }
217
+
218
+ // Store the actual port on the server instance for reference
219
+ server_instance.port = actual_port;
220
+
199
221
  console.log('🔍 DEBUG: Calling server_instance.start()');
200
- server_instance.start(port, (err) => {
222
+ server_instance.start(actual_port, (err) => {
201
223
  if (err) {
202
224
  console.log('🔍 DEBUG: server_instance.start() failed:', err);
203
225
  return settle(reject, err);
204
226
  }
205
227
  console.log('🔍 DEBUG: server_instance.start() succeeded');
206
- const message = host ? `Serving on http://${Array.isArray(host) ? host[0] : host}:${port || 0}/` : `Serving on port ${port || 0} (all IPv4 interfaces)`;
228
+ const message = host ? `Serving on http://${Array.isArray(host) ? host[0] : host}:${actual_port}/` : `Serving on port ${actual_port} (all IPv4 interfaces)`;
207
229
  console.log(message);
208
230
  console.log('Server ready');
209
231
  settle(resolve, server_instance);
package/tests/README.md CHANGED
@@ -22,12 +22,13 @@ tests/
22
22
  ├── publishers.test.js # Component isolation tests for publishers
23
23
  ├── configuration-validation.test.js # Configuration validation tests
24
24
  ├── end-to-end.test.js # Full integration tests
25
- ├── content-analysis.test.js # Content analysis and verification
26
- ├── performance.test.js # Performance benchmarks
27
- ├── error-handling.test.js # Error handling and edge cases
28
- ├── test-runner.js # Custom test runner with reporting
29
- └── README.md # This file
30
- ```
25
+ ├── content-analysis.test.js # Content analysis and verification
26
+ ├── performance.test.js # Performance benchmarks
27
+ ├── error-handling.test.js # Error handling and edge cases
28
+ ├── examples-controls.e2e.test.js # Example apps regression (controls)
29
+ ├── test-runner.js # Custom test runner with reporting
30
+ └── README.md # This file
31
+ ```
31
32
 
32
33
  ## Running Tests
33
34
 
@@ -36,19 +37,24 @@ tests/
36
37
  npm test
37
38
  ```
38
39
 
39
- ### Run Specific Test Suite
40
- ```bash
41
- # Using the custom test runner
42
- node tests/test-runner.js --test=bundlers.test.js
43
-
44
- # Using mocha directly
45
- npx mocha tests/bundlers.test.js
46
- ```
47
-
48
- ### Run Tests with Options
49
- ```bash
50
- # Debug mode (enables sourcemaps)
51
- node tests/test-runner.js --debug
40
+ ### Run Specific Test Suite
41
+ ```bash
42
+ # Using the custom test runner
43
+ node tests/test-runner.js --test=bundlers.test.js
44
+
45
+ # Using mocha directly
46
+ npx mocha tests/bundlers.test.js
47
+ ```
48
+
49
+ ### Run Example Apps Regression Suite
50
+ ```bash
51
+ npm run test:examples:controls
52
+ ```
53
+
54
+ ### Run Tests with Options
55
+ ```bash
56
+ # Debug mode (enables sourcemaps)
57
+ node tests/test-runner.js --debug
52
58
 
53
59
  # Verbose output
54
60
  node tests/test-runner.js --verbose
@@ -113,12 +119,20 @@ Comprehensive error scenario testing:
113
119
 
114
120
  - Invalid JavaScript syntax
115
121
  - File system errors (permissions, missing files)
116
- - Configuration validation errors
117
- - Network and HTTP errors
118
- - Memory and performance limits
119
- - Encoding issues
120
-
121
- ## Configuration Examples
122
+ - Configuration validation errors
123
+ - Network and HTTP errors
124
+ - Memory and performance limits
125
+ - Encoding issues
126
+
127
+ ### 7. Examples/Controls E2E Tests (`examples-controls.e2e.test.js`)
128
+
129
+ Regression coverage for a representative set of `examples/controls/*` apps:
130
+
131
+ - Boots a server per example control
132
+ - Verifies `/`, `/js/js.js`, and `/css/css.css` routes
133
+ - Ensures HTML rendering works without `Accept-Encoding`
134
+
135
+ ## Configuration Examples
122
136
 
123
137
  ### Basic Minification
124
138
  ```javascript
@@ -247,4 +261,4 @@ The test suite is designed to work with continuous integration:
247
261
  path: test-report.json
248
262
  ```
249
263
 
250
- This ensures the minification, compression, and sourcemap features maintain their functionality across code changes.
264
+ This ensures the minification, compression, and sourcemap features maintain their functionality across code changes.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * E2E regression tests for example control apps under examples/controls/.
3
+ *
4
+ * These tests boot a server for a selected set of examples and validate:
5
+ * - The HTML route renders successfully (even without Accept-Encoding)
6
+ * - Bundled JS and CSS routes are served
7
+ * - The server-rendered HTML includes the expected number of Window controls
8
+ */
9
+
10
+ const assert = require('assert');
11
+ const http = require('http');
12
+ const path = require('path');
13
+ const { describe, it } = require('mocha');
14
+
15
+ const Server = require('../server');
16
+ const { get_free_port } = require('../port-utils');
17
+
18
+ const repo_root_path = path.join(__dirname, '..');
19
+ const examples_controls_root_path = path.join(repo_root_path, 'examples', 'controls');
20
+
21
+ const examples = [
22
+ { dir_name: '1) window', ctrl_name: 'Demo_UI', expected_window_count: 1 },
23
+ { dir_name: '2) two windows', ctrl_name: 'Demo_UI', expected_window_count: 2 },
24
+ { dir_name: '3) five windows', ctrl_name: 'Demo_UI', expected_window_count: 5 },
25
+ { dir_name: '4) window, tabbed panel', ctrl_name: 'Demo_UI', expected_window_count: 1 },
26
+ { dir_name: '5) window, grid', ctrl_name: 'Demo_UI', expected_window_count: 1 },
27
+ { dir_name: '8) window, checkbox/a)', ctrl_name: 'Demo_UI', expected_window_count: 1 },
28
+ { dir_name: '9) window, date picker', ctrl_name: 'Demo_UI', expected_window_count: 1 },
29
+ { dir_name: '12) window, Select_Options control', ctrl_name: 'Demo_UI', expected_window_count: 1 },
30
+ { dir_name: '13) window, Dropdown_Menu control', ctrl_name: 'Demo_UI', expected_window_count: 1 }
31
+ ];
32
+
33
+ function make_request(url, { headers = {} } = {}) {
34
+ return new Promise((resolve, reject) => {
35
+ const parsed_url = new URL(url);
36
+ const options = {
37
+ hostname: parsed_url.hostname,
38
+ port: parsed_url.port,
39
+ path: parsed_url.pathname,
40
+ method: 'GET',
41
+ headers: {
42
+ 'User-Agent': 'JSGUI3-Examples-E2E/1.0',
43
+ ...headers
44
+ }
45
+ };
46
+
47
+ const req = http.request(options, (res) => {
48
+ const chunks = [];
49
+ res.on('data', (chunk) => chunks.push(chunk));
50
+ res.on('end', () => {
51
+ resolve({
52
+ status_code: res.statusCode,
53
+ headers: res.headers,
54
+ body: Buffer.concat(chunks).toString('utf8')
55
+ });
56
+ });
57
+ });
58
+
59
+ req.on('error', reject);
60
+ req.setTimeout(15000, () => {
61
+ req.destroy();
62
+ reject(new Error('Request timeout'));
63
+ });
64
+ req.end();
65
+ });
66
+ }
67
+
68
+ function count_occurrences(haystack, needle) {
69
+ let idx = 0;
70
+ let count = 0;
71
+ while (true) {
72
+ idx = haystack.indexOf(needle, idx);
73
+ if (idx === -1) return count;
74
+ count++;
75
+ idx += needle.length;
76
+ }
77
+ }
78
+
79
+ async function start_example_server({ dir_name, ctrl_name }) {
80
+ const example_dir_path = path.join(examples_controls_root_path, dir_name);
81
+ const example_client_path = path.join(example_dir_path, 'client.js');
82
+
83
+ const jsgui = require(example_client_path);
84
+ const ctrl = jsgui.controls && jsgui.controls[ctrl_name];
85
+ assert(ctrl, `Missing exported control jsgui.controls.${ctrl_name} in ${example_client_path}`);
86
+
87
+ const server = new Server({
88
+ Ctrl: ctrl,
89
+ src_path_client_js: example_client_path,
90
+ name: `examples/controls/${dir_name}`
91
+ });
92
+
93
+ server.allowed_addresses = ['127.0.0.1'];
94
+
95
+ await new Promise((resolve, reject) => {
96
+ const timeout = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
97
+ server.on('ready', () => {
98
+ clearTimeout(timeout);
99
+ resolve();
100
+ });
101
+ });
102
+
103
+ const port = await get_free_port();
104
+
105
+ await new Promise((resolve, reject) => {
106
+ server.start(port, (err) => (err ? reject(err) : resolve()));
107
+ });
108
+
109
+ return { server, port };
110
+ }
111
+
112
+ async function close_server(server) {
113
+ await new Promise((resolve) => server.close(resolve));
114
+ }
115
+
116
+ describe('Examples/Controls E2E Regression Tests', function() {
117
+ this.timeout(180000);
118
+
119
+ for (const example of examples) {
120
+ it(`should render and serve bundles for "${example.dir_name}"`, async function() {
121
+ const { server, port } = await start_example_server(example);
122
+
123
+ try {
124
+ const base_url = `http://127.0.0.1:${port}`;
125
+
126
+ const html_response = await make_request(`${base_url}/`, {
127
+ headers: {
128
+ // Deliberately omit Accept-Encoding to ensure identity works.
129
+ }
130
+ });
131
+
132
+ assert.strictEqual(html_response.status_code, 200);
133
+ assert(
134
+ (html_response.headers['content-type'] || '').includes('text/html'),
135
+ 'Expected HTML content-type'
136
+ );
137
+
138
+ const window_count = count_occurrences(html_response.body, 'data-jsgui-type="window"');
139
+ assert.strictEqual(
140
+ window_count,
141
+ example.expected_window_count,
142
+ `Expected ${example.expected_window_count} windows, got ${window_count}`
143
+ );
144
+
145
+ const js_response = await make_request(`${base_url}/js/js.js`);
146
+ assert.strictEqual(js_response.status_code, 200);
147
+ assert(
148
+ (js_response.headers['content-type'] || '').includes('javascript'),
149
+ 'Expected JavaScript content-type'
150
+ );
151
+ assert(js_response.body.length > 1000, 'Expected non-trivial JS bundle');
152
+
153
+ const css_response = await make_request(`${base_url}/css/css.css`);
154
+ assert.strictEqual(css_response.status_code, 200);
155
+ assert(
156
+ (css_response.headers['content-type'] || '').includes('css'),
157
+ 'Expected CSS content-type'
158
+ );
159
+ } finally {
160
+ await close_server(server);
161
+ }
162
+ });
163
+ }
164
+ });