jsgui3-server 0.0.150 → 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.
Files changed (86) hide show
  1. package/.github/instructions/copilot.instructions.md +1 -0
  2. package/AGENTS.md +2 -0
  3. package/README.md +89 -13
  4. package/admin-ui/v1/controls/admin_shell.js +702 -669
  5. package/admin-ui/v1/server.js +14 -1
  6. package/docs/api-reference.md +504 -306
  7. package/docs/books/creating-a-new-admin-ui/README.md +20 -20
  8. package/docs/books/website-design/01-introduction.md +73 -0
  9. package/docs/books/website-design/02-current-state.md +195 -0
  10. package/docs/books/website-design/03-base-class.md +181 -0
  11. package/docs/books/website-design/04-webpage.md +307 -0
  12. package/docs/books/website-design/05-website.md +456 -0
  13. package/docs/books/website-design/06-pages-storage.md +170 -0
  14. package/docs/books/website-design/07-api-layer.md +285 -0
  15. package/docs/books/website-design/08-server-integration.md +271 -0
  16. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  17. package/docs/books/website-design/10-open-questions.md +196 -0
  18. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  19. package/docs/books/website-design/12-content-model.md +395 -0
  20. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  21. package/docs/books/website-design/14-website-module-spec.md +541 -0
  22. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  23. package/docs/books/website-design/16-minimal-first.md +203 -0
  24. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  25. package/docs/books/website-design/README.md +43 -0
  26. package/docs/comprehensive-documentation.md +220 -220
  27. package/docs/configuration-reference.md +281 -204
  28. package/docs/middleware-guide.md +236 -0
  29. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  30. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  31. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  32. package/docs/swagger.md +316 -0
  33. package/docs/system-architecture.md +24 -18
  34. package/examples/controls/1) window/server.js +6 -1
  35. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  36. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  37. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  38. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  39. declarative api/e2e-screenshot-1-name-change.png +0 -0
  40. declarative api/e2e-screenshot-2-toggled.png +0 -0
  41. declarative api/e2e-screenshot-3-final.png +0 -0
  42. declarative api/e2e-screenshot-final.png +0 -0
  43. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  44. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  45. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  46. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  47. package/examples/data-views/01) query-endpoint/server.js +61 -0
  48. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  49. package/labs/website-design/002-pages-storage/check.js +244 -0
  50. package/labs/website-design/002-pages-storage/results.txt +0 -0
  51. package/labs/website-design/003-type-detection/check.js +193 -0
  52. package/labs/website-design/003-type-detection/results.txt +0 -0
  53. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  54. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  55. package/labs/website-design/005-normalize-input/check.js +303 -0
  56. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  57. package/labs/website-design/README.md +34 -0
  58. package/labs/website-design/manifest.json +68 -0
  59. package/labs/website-design/run-all.js +60 -0
  60. package/middleware/compression.js +217 -0
  61. package/middleware/index.js +15 -0
  62. package/middleware/json-body.js +126 -0
  63. package/module.js +3 -0
  64. package/openapi.js +474 -0
  65. package/package.json +11 -8
  66. package/publishers/Publishers.js +6 -5
  67. package/publishers/http-function-publisher.js +135 -126
  68. package/publishers/http-webpage-publisher.js +89 -11
  69. package/publishers/query-publisher.js +116 -0
  70. package/publishers/swagger-publisher.js +203 -0
  71. package/publishers/swagger-ui.js +578 -0
  72. package/resources/adapters/array-adapter.js +143 -0
  73. package/resources/query-resource.js +131 -0
  74. package/serve-factory.js +756 -18
  75. package/server.js +502 -123
  76. package/tests/README.md +23 -1
  77. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  78. package/tests/helpers/playwright-e2e-harness.js +326 -0
  79. package/tests/openapi.test.js +319 -0
  80. package/tests/playwright-smoke.test.js +134 -0
  81. package/tests/publish-enhancements.test.js +673 -0
  82. package/tests/query-publisher.test.js +430 -0
  83. package/tests/quick-json-body-test.js +169 -0
  84. package/tests/serve.test.js +425 -122
  85. package/tests/swagger-publisher.test.js +1076 -0
  86. package/tests/test-runner.js +1 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Tests for the OpenAPI / Swagger integration.
3
+ *
4
+ * Covers:
5
+ * - OpenAPI spec generation from server._api_registry
6
+ * - Schema conversion (simple schema → OpenAPI schema)
7
+ * - Swagger HTML generation
8
+ * - End-to-end route serving (/api/openapi.json, /api/docs)
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const assert = require('assert');
14
+ const http = require('http');
15
+ const net = require('net');
16
+ const { describe, it, before, after } = require('node:test');
17
+
18
+ // ── Unit tests for openapi.js ────────────────────────────────
19
+
20
+ const { generate_openapi_spec, collect_api_entries, simple_schema_to_openapi } = require('../openapi');
21
+ const { generate_swagger_html } = require('../publishers/swagger-ui');
22
+
23
+ describe('simple_schema_to_openapi', () => {
24
+ it('should convert a flat params map to an OpenAPI schema', () => {
25
+ const schema = simple_schema_to_openapi({
26
+ page: { type: 'integer', description: 'Page number', default: 1 },
27
+ name: { type: 'string', description: 'User name' }
28
+ });
29
+
30
+ assert.strictEqual(schema.type, 'object');
31
+ assert.strictEqual(schema.properties.page.type, 'integer');
32
+ assert.strictEqual(schema.properties.page.description, 'Page number');
33
+ assert.strictEqual(schema.properties.page.default, 1);
34
+ assert.strictEqual(schema.properties.name.type, 'string');
35
+ });
36
+
37
+ it('should pass through a raw type definition', () => {
38
+ const schema = simple_schema_to_openapi({ type: 'array', items: { type: 'string' } });
39
+ assert.strictEqual(schema.type, 'array');
40
+ assert.deepStrictEqual(schema.items, { type: 'string' });
41
+ });
42
+
43
+ it('should handle null / undefined gracefully', () => {
44
+ assert.strictEqual(simple_schema_to_openapi(null), null);
45
+ assert.strictEqual(simple_schema_to_openapi(undefined), null);
46
+ });
47
+
48
+ it('should collect required fields', () => {
49
+ const schema = simple_schema_to_openapi({
50
+ id: { type: 'integer', required: true },
51
+ name: { type: 'string' }
52
+ });
53
+ assert.deepStrictEqual(schema.required, ['id']);
54
+ });
55
+ });
56
+
57
+ describe('generate_openapi_spec', () => {
58
+ it('should produce a valid minimal spec from a mock server', () => {
59
+ const mock_server = {
60
+ name: 'Test Server',
61
+ _api_registry: [
62
+ {
63
+ path: '/api/users',
64
+ method: 'POST',
65
+ meta: {
66
+ summary: 'List users',
67
+ description: 'Returns all users',
68
+ tags: ['Users'],
69
+ params: {
70
+ page: { type: 'integer', default: 1 }
71
+ },
72
+ returns: {
73
+ rows: { type: 'array', items: { type: 'object' } },
74
+ total_count: { type: 'integer' }
75
+ }
76
+ },
77
+ schema: {}
78
+ }
79
+ ],
80
+ get_listening_endpoints: () => []
81
+ };
82
+
83
+ const spec = generate_openapi_spec(mock_server);
84
+
85
+ assert.strictEqual(spec.openapi, '3.0.3');
86
+ assert.strictEqual(spec.info.title, 'Test Server');
87
+ assert.ok(spec.paths['/api/users']);
88
+ assert.ok(spec.paths['/api/users'].post);
89
+ assert.strictEqual(spec.paths['/api/users'].post.summary, 'List users');
90
+ assert.deepStrictEqual(spec.paths['/api/users'].post.tags, ['Users']);
91
+
92
+ // Request body should exist for POST
93
+ assert.ok(spec.paths['/api/users'].post.requestBody);
94
+ const req_schema = spec.paths['/api/users'].post.requestBody.content['application/json'].schema;
95
+ assert.strictEqual(req_schema.type, 'object');
96
+ assert.strictEqual(req_schema.properties.page.type, 'integer');
97
+
98
+ // Response schema
99
+ const res_schema = spec.paths['/api/users'].post.responses['200'].content['application/json'].schema;
100
+ assert.strictEqual(res_schema.type, 'object');
101
+ assert.ok(res_schema.properties.rows);
102
+ assert.ok(res_schema.properties.total_count);
103
+ });
104
+
105
+ it('should produce a valid spec even with zero metadata', () => {
106
+ const mock_server = {
107
+ name: 'Bare Server',
108
+ _api_registry: [
109
+ {
110
+ path: '/api/ping',
111
+ method: 'GET',
112
+ meta: {},
113
+ schema: {}
114
+ }
115
+ ],
116
+ get_listening_endpoints: () => []
117
+ };
118
+
119
+ const spec = generate_openapi_spec(mock_server);
120
+ assert.strictEqual(spec.openapi, '3.0.3');
121
+ assert.ok(spec.paths['/api/ping']);
122
+ assert.ok(spec.paths['/api/ping'].get);
123
+ assert.ok(spec.paths['/api/ping'].get.responses['200']);
124
+ });
125
+
126
+ it('should skip swagger own routes', () => {
127
+ const mock_server = {
128
+ name: 'Test',
129
+ _api_registry: [
130
+ { path: '/api/openapi.json', method: 'GET', meta: {} },
131
+ { path: '/api/docs', method: 'GET', meta: {} },
132
+ { path: '/api/hello', method: 'GET', meta: {} }
133
+ ],
134
+ get_listening_endpoints: () => []
135
+ };
136
+
137
+ const spec = generate_openapi_spec(mock_server);
138
+ assert.ok(!spec.paths['/api/openapi.json']);
139
+ assert.ok(!spec.paths['/api/docs']);
140
+ assert.ok(spec.paths['/api/hello']);
141
+ });
142
+ });
143
+
144
+ describe('collect_api_entries', () => {
145
+ it('should merge entries from _api_registry and function_publishers', () => {
146
+ const mock_server = {
147
+ _api_registry: [
148
+ { path: '/api/a', method: 'POST', meta: { summary: 'A' } }
149
+ ],
150
+ function_publishers: [
151
+ {
152
+ name: 'b',
153
+ api_meta: { summary: 'B' },
154
+ schema: {}
155
+ }
156
+ ]
157
+ };
158
+
159
+ const entries = collect_api_entries(mock_server);
160
+ assert.strictEqual(entries.length, 2);
161
+ });
162
+
163
+ it('should not duplicate if same route exists in both sources', () => {
164
+ const mock_server = {
165
+ _api_registry: [
166
+ { path: '/api/a', method: 'POST', meta: { summary: 'A from registry' } }
167
+ ],
168
+ function_publishers: [
169
+ {
170
+ name: 'a',
171
+ api_meta: { summary: 'A from publisher', method: 'POST' },
172
+ schema: {}
173
+ }
174
+ ]
175
+ };
176
+
177
+ const entries = collect_api_entries(mock_server);
178
+ // Registry takes precedence
179
+ assert.strictEqual(entries.length, 1);
180
+ assert.strictEqual(entries[0].meta.summary, 'A from registry');
181
+ });
182
+ });
183
+
184
+ describe('generate_swagger_html', () => {
185
+ it('should return valid HTML with swagger-ui CDN links', () => {
186
+ const html = generate_swagger_html({ title: 'My API' });
187
+ assert.ok(html.includes('<!doctype html>'));
188
+ assert.ok(html.includes('swagger-ui-bundle.js'));
189
+ assert.ok(html.includes('swagger-ui.css'));
190
+ assert.ok(html.includes('/api/openapi.json'));
191
+ assert.ok(html.includes('My API'));
192
+ });
193
+
194
+ it('should use custom spec_url', () => {
195
+ const html = generate_swagger_html({ spec_url: '/custom/spec.json' });
196
+ assert.ok(html.includes('/custom/spec.json'));
197
+ });
198
+ });
199
+
200
+ // ── Integration test: actual HTTP server ─────────────────────
201
+
202
+ const get_free_port = () => new Promise((resolve, reject) => {
203
+ const srv = net.createServer();
204
+ srv.listen(0, '127.0.0.1', () => {
205
+ const port = srv.address().port;
206
+ srv.close(() => resolve(port));
207
+ });
208
+ srv.on('error', reject);
209
+ });
210
+
211
+ const http_get = (port, path) => new Promise((resolve, reject) => {
212
+ const req = http.get(`http://127.0.0.1:${port}${path}`, (res) => {
213
+ let body = '';
214
+ res.on('data', (chunk) => body += chunk);
215
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
216
+ });
217
+ req.on('error', reject);
218
+ });
219
+
220
+ describe('Swagger routes integration (Server.serve)', function () {
221
+ let server_instance;
222
+ let port;
223
+
224
+ // Inject fake publishers to avoid jsgui3-html bundling
225
+ const path = require('path');
226
+ const fake_webpage_publisher_path = require.resolve('../publishers/http-webpage-publisher');
227
+ const fake_website_publisher_path = require.resolve('../publishers/http-webpageorsite-publisher');
228
+ const original_webpage = require.cache[fake_webpage_publisher_path];
229
+ const original_website = require.cache[fake_website_publisher_path];
230
+
231
+ const { Evented_Class } = require('lang-tools');
232
+
233
+ class FakePublisher extends Evented_Class {
234
+ constructor() {
235
+ super();
236
+ const self = this;
237
+ setImmediate(() => self.raise('ready', { _arr: [] }));
238
+ }
239
+ handle_http(req, res) {
240
+ res.writeHead(200, { 'Content-Type': 'text/html' });
241
+ res.end('<html><body>fake</body></html>');
242
+ }
243
+ meets_requirements() { return true; }
244
+ start(cb) { cb && cb(null); }
245
+ stop(cb) { cb && cb(null); }
246
+ }
247
+
248
+ require.cache[fake_webpage_publisher_path] = { exports: FakePublisher };
249
+ require.cache[fake_website_publisher_path] = { exports: FakePublisher };
250
+
251
+ const Server = require('../server');
252
+
253
+ before(async () => {
254
+ port = await get_free_port();
255
+
256
+ server_instance = await Server.serve({
257
+ host: '127.0.0.1',
258
+ port,
259
+ swagger: true,
260
+ website: false,
261
+ api: {
262
+ 'test/hello': {
263
+ handler: (input) => ({ message: 'Hello', input }),
264
+ method: 'POST',
265
+ summary: 'Say hello',
266
+ description: 'Returns a greeting',
267
+ tags: ['Test'],
268
+ params: {
269
+ name: { type: 'string', description: 'Your name' }
270
+ },
271
+ returns: {
272
+ message: { type: 'string' }
273
+ }
274
+ },
275
+ 'test/ping': {
276
+ handler: () => ({ pong: true }),
277
+ method: 'GET',
278
+ summary: 'Ping endpoint'
279
+ }
280
+ }
281
+ });
282
+ });
283
+
284
+ after(async () => {
285
+ if (original_webpage) require.cache[fake_webpage_publisher_path] = original_webpage;
286
+ if (original_website) require.cache[fake_website_publisher_path] = original_website;
287
+ if (server_instance && typeof server_instance.close === 'function') {
288
+ await new Promise(r => server_instance.close(r));
289
+ }
290
+ });
291
+
292
+ it('GET /api/openapi.json should return valid OpenAPI spec', async () => {
293
+ const { status, headers, body } = await http_get(port, '/api/openapi.json');
294
+ assert.strictEqual(status, 200);
295
+ assert.ok(headers['content-type'].includes('application/json'));
296
+
297
+ const spec = JSON.parse(body);
298
+ assert.strictEqual(spec.openapi, '3.0.3');
299
+ assert.ok(spec.paths['/api/test/hello']);
300
+ assert.ok(spec.paths['/api/test/ping']);
301
+ assert.strictEqual(spec.paths['/api/test/hello'].post.summary, 'Say hello');
302
+ assert.deepStrictEqual(spec.paths['/api/test/hello'].post.tags, ['Test']);
303
+ });
304
+
305
+ it('GET /api/docs should return Swagger UI HTML', async () => {
306
+ const { status, headers, body } = await http_get(port, '/api/docs');
307
+ assert.strictEqual(status, 200);
308
+ assert.ok(headers['content-type'].includes('text/html'));
309
+ assert.ok(body.includes('swagger-ui'));
310
+ assert.ok(body.includes('/api/openapi.json'));
311
+ });
312
+
313
+ it('spec should not include its own documentation routes', async () => {
314
+ const { body } = await http_get(port, '/api/openapi.json');
315
+ const spec = JSON.parse(body);
316
+ assert.ok(!spec.paths['/api/openapi.json']);
317
+ assert.ok(!spec.paths['/api/docs']);
318
+ });
319
+ });
@@ -0,0 +1,134 @@
1
+ const assert = require('assert');
2
+ const path = require('path');
3
+ const { describe, it, before, after } = require('mocha');
4
+
5
+ const Server = require('../server');
6
+ const { get_free_port } = require('../port-utils');
7
+ const {
8
+ ensure_playwright_module,
9
+ launch_playwright_browser,
10
+ open_page,
11
+ close_page_with_probe,
12
+ stop_server_instance,
13
+ assert_clean_page_probe
14
+ } = require('./helpers/playwright-e2e-harness');
15
+
16
+ const button_fixture_client_path = path.join(__dirname, 'fixtures', 'bundling-default-button-client.js');
17
+
18
+ const load_fixture_ctrl = (client_path, ctrl_name) => {
19
+ const resolved_client_path = require.resolve(client_path);
20
+ delete require.cache[resolved_client_path];
21
+
22
+ const fixture_module = require(resolved_client_path);
23
+ const ctrl_constructor = fixture_module.controls && fixture_module.controls[ctrl_name];
24
+ assert(ctrl_constructor, `Missing exported control jsgui.controls.${ctrl_name} in ${client_path}`);
25
+ return ctrl_constructor;
26
+ };
27
+
28
+ const start_fixture_server = async ({ client_path, ctrl_name }) => {
29
+ const ctrl_constructor = load_fixture_ctrl(client_path, ctrl_name);
30
+
31
+ const server_instance = new Server({
32
+ Ctrl: ctrl_constructor,
33
+ src_path_client_js: client_path,
34
+ name: `tests/playwright/${ctrl_name}`
35
+ });
36
+
37
+ server_instance.allowed_addresses = ['127.0.0.1'];
38
+
39
+ await new Promise((resolve, reject) => {
40
+ const timeout_handle = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
41
+ server_instance.on('ready', () => {
42
+ clearTimeout(timeout_handle);
43
+ resolve();
44
+ });
45
+ });
46
+
47
+ const port = await get_free_port();
48
+ await new Promise((resolve, reject) => {
49
+ server_instance.start(port, (error) => {
50
+ if (error) reject(error);
51
+ else resolve();
52
+ });
53
+ });
54
+
55
+ return {
56
+ server_instance,
57
+ port
58
+ };
59
+ };
60
+
61
+ describe('Playwright Smoke Tests', function () {
62
+ this.timeout(240000);
63
+
64
+ let playwright_module = null;
65
+ let browser_instance = null;
66
+
67
+ before(async function () {
68
+ this.timeout(60000);
69
+
70
+ playwright_module = ensure_playwright_module();
71
+ if (!playwright_module) {
72
+ this.skip();
73
+ return;
74
+ }
75
+
76
+ try {
77
+ browser_instance = await launch_playwright_browser(playwright_module);
78
+ } catch {
79
+ this.skip();
80
+ }
81
+ });
82
+
83
+ after(async function () {
84
+ if (browser_instance) {
85
+ await browser_instance.close();
86
+ browser_instance = null;
87
+ }
88
+ });
89
+
90
+ it('opens a locally served page and verifies core webpage assets', async function () {
91
+ let server_instance = null;
92
+ let page = null;
93
+ let page_probe = null;
94
+
95
+ try {
96
+ const started_server = await start_fixture_server({
97
+ client_path: button_fixture_client_path,
98
+ ctrl_name: 'Bundling_Default_Button_App'
99
+ });
100
+
101
+ server_instance = started_server.server_instance;
102
+
103
+ const open_result = await open_page(
104
+ browser_instance,
105
+ `http://127.0.0.1:${started_server.port}/`,
106
+ { wait_until: 'domcontentloaded' }
107
+ );
108
+ page = open_result.page;
109
+ page_probe = open_result.page_probe;
110
+
111
+ await page.waitForSelector('[data-test="bundle-test-button"]');
112
+
113
+ const button_text = await page.textContent('[data-test="bundle-test-button"]');
114
+ assert.strictEqual((button_text || '').trim(), 'Run', 'Expected rendered button text from fixture control');
115
+
116
+ const bundle_statuses = await page.evaluate(async () => {
117
+ const js_response = await fetch('/js/js.js', { cache: 'no-store' });
118
+ const css_response = await fetch('/css/css.css', { cache: 'no-store' });
119
+ return {
120
+ js_status: js_response.status,
121
+ css_status: css_response.status
122
+ };
123
+ });
124
+
125
+ assert.strictEqual(bundle_statuses.js_status, 200, 'Expected JavaScript bundle to be served');
126
+ assert.strictEqual(bundle_statuses.css_status, 200, 'Expected CSS bundle to be served');
127
+
128
+ assert_clean_page_probe(page_probe);
129
+ } finally {
130
+ await close_page_with_probe(page, page_probe);
131
+ await stop_server_instance(server_instance);
132
+ }
133
+ });
134
+ });