jsgui3-server 0.0.151 → 0.0.155

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 (109) hide show
  1. package/README.md +21 -0
  2. package/admin-ui/v1/controls/admin_shell.js +33 -0
  3. package/admin-ui/v1/server.js +14 -1
  4. package/docs/agi/skills/README.md +23 -0
  5. package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
  6. package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
  7. package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
  8. package/docs/agi/skills/deep-research/SKILL.md +156 -0
  9. package/docs/agi/skills/endurance/SKILL.md +53 -0
  10. package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
  11. package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
  12. package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
  13. package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
  14. package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
  15. package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
  16. package/docs/agi/skills/session-discipline/SKILL.md +40 -0
  17. package/docs/agi/skills/skill-writing/SKILL.md +211 -0
  18. package/docs/agi/skills/static-analysis/SKILL.md +58 -0
  19. package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
  20. package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
  21. package/docs/api-reference.md +120 -2
  22. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
  23. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
  24. package/docs/books/website-design/01-introduction.md +73 -0
  25. package/docs/books/website-design/02-current-state.md +195 -0
  26. package/docs/books/website-design/03-base-class.md +181 -0
  27. package/docs/books/website-design/04-webpage.md +307 -0
  28. package/docs/books/website-design/05-website.md +456 -0
  29. package/docs/books/website-design/06-pages-storage.md +170 -0
  30. package/docs/books/website-design/07-api-layer.md +285 -0
  31. package/docs/books/website-design/08-server-integration.md +271 -0
  32. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  33. package/docs/books/website-design/10-open-questions.md +196 -0
  34. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  35. package/docs/books/website-design/12-content-model.md +395 -0
  36. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  37. package/docs/books/website-design/14-website-module-spec.md +541 -0
  38. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  39. package/docs/books/website-design/16-minimal-first.md +203 -0
  40. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  41. package/docs/books/website-design/README.md +43 -0
  42. package/docs/bundling-system-deep-dive.md +112 -3
  43. package/docs/configuration-reference.md +84 -0
  44. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  45. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  46. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  47. package/docs/swagger.md +316 -0
  48. package/examples/controls/1) window/server.js +6 -1
  49. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  50. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  51. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  52. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  53. declarative api/e2e-screenshot-1-name-change.png +0 -0
  54. declarative api/e2e-screenshot-2-toggled.png +0 -0
  55. declarative api/e2e-screenshot-3-final.png +0 -0
  56. declarative api/e2e-screenshot-final.png +0 -0
  57. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  58. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  59. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  60. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  61. package/examples/data-views/01) query-endpoint/server.js +61 -0
  62. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  63. package/labs/website-design/002-pages-storage/check.js +244 -0
  64. package/labs/website-design/002-pages-storage/results.txt +0 -0
  65. package/labs/website-design/003-type-detection/check.js +193 -0
  66. package/labs/website-design/003-type-detection/results.txt +0 -0
  67. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  68. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  69. package/labs/website-design/005-normalize-input/check.js +303 -0
  70. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  71. package/labs/website-design/README.md +34 -0
  72. package/labs/website-design/manifest.json +68 -0
  73. package/labs/website-design/run-all.js +60 -0
  74. package/middleware/json-body.js +126 -0
  75. package/openapi.js +474 -0
  76. package/package.json +13 -7
  77. package/publishers/Publishers.js +6 -5
  78. package/publishers/http-function-publisher.js +135 -126
  79. package/publishers/http-webpage-publisher.js +89 -11
  80. package/publishers/query-publisher.js +116 -0
  81. package/publishers/swagger-publisher.js +203 -0
  82. package/publishers/swagger-ui.js +578 -0
  83. package/resources/adapters/array-adapter.js +143 -0
  84. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
  85. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
  86. package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
  87. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
  88. package/resources/query-resource.js +131 -0
  89. package/serve-factory.js +677 -18
  90. package/server.js +585 -167
  91. package/tests/README.md +86 -2
  92. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  93. package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
  94. package/tests/control-elimination-root-feature-pruning.test.js +440 -0
  95. package/tests/control-elimination-static-bracket-access.test.js +245 -0
  96. package/tests/control-scan-manifest-regression.test.js +2 -0
  97. package/tests/end-to-end.test.js +22 -21
  98. package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
  99. package/tests/helpers/playwright-e2e-harness.js +326 -0
  100. package/tests/helpers/puppeteer-e2e-harness.js +62 -1
  101. package/tests/openapi.test.js +319 -0
  102. package/tests/playwright-smoke.test.js +134 -0
  103. package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
  104. package/tests/publish-enhancements.test.js +673 -0
  105. package/tests/query-publisher.test.js +430 -0
  106. package/tests/quick-json-body-test.js +169 -0
  107. package/tests/serve.test.js +425 -122
  108. package/tests/swagger-publisher.test.js +1076 -0
  109. package/tests/test-runner.js +4 -0
package/openapi.js ADDED
@@ -0,0 +1,474 @@
1
+ /**
2
+ * OpenAPI 3.0.3 Specification Generator for jsgui3-server.
3
+ *
4
+ * This module produces a valid OpenAPI 3.0.3 specification from the
5
+ * server's registered API endpoints. It is the heart of jsgui3-server's
6
+ * built-in Swagger support — no external dependencies are needed.
7
+ *
8
+ * ## How It Works
9
+ *
10
+ * 1. **Data collection** — {@link collect_api_entries} walks three sources
11
+ * on the server instance, in priority order:
12
+ * - `server._api_registry` — entries added by {@link JSGUI_Single_Process_Server#publish}
13
+ * - `server.function_publishers` — raw Function_Publisher instances
14
+ * - `server.website_manifest.api_endpoints` — endpoints declared in `Server.serve()`
15
+ * Duplicates (same `METHOD + path`) are resolved by source priority.
16
+ *
17
+ * 2. **Schema conversion** — {@link simple_schema_to_openapi} converts the
18
+ * lightweight `{key: {type, description, default}}` format that jsgui3
19
+ * uses for `params` / `returns` into proper OpenAPI Schema Objects.
20
+ *
21
+ * 3. **Spec assembly** — {@link generate_openapi_spec} combines everything
22
+ * into a complete OpenAPI 3.0.3 document with `info`, `servers`,
23
+ * `paths`, and `tags`.
24
+ *
25
+ * ## Metadata Format
26
+ *
27
+ * Endpoints can provide metadata via the `meta` argument to `server.publish()`:
28
+ *
29
+ * ```js
30
+ * server.publish('users/list', handler, {
31
+ * method: 'POST', // HTTP method (default: 'POST')
32
+ * summary: 'List all users', // One-line summary
33
+ * description: 'Long description', // Multi-line Markdown description
34
+ * tags: ['Users'], // Grouping tags
35
+ * deprecated: false, // Mark as deprecated
36
+ * operationId: 'listUsers', // Custom operation ID
37
+ * params: { // Request body schema
38
+ * page: { type: 'integer', description: 'Page number', default: 1 },
39
+ * name: { type: 'string', description: 'Filter by name' }
40
+ * },
41
+ * returns: { // Response body schema
42
+ * rows: { type: 'array', items: { type: 'object' } },
43
+ * total_count: { type: 'integer' }
44
+ * },
45
+ * response_description: 'List of users with pagination'
46
+ * });
47
+ * ```
48
+ *
49
+ * All metadata fields are **optional**. Endpoints without metadata still
50
+ * produce a valid (minimal) OpenAPI entry.
51
+ *
52
+ * @module openapi
53
+ * @see {@link module:publishers/swagger-ui} for the UI that renders this spec.
54
+ */
55
+
56
+ 'use strict';
57
+
58
+ // ── Schema Helpers ───────────────────────────────────────────
59
+
60
+ /**
61
+ * Set of valid OpenAPI primitive type names.
62
+ *
63
+ * Any type string not in this set is normalised to `"string"`.
64
+ *
65
+ * @const {Set<string>}
66
+ * @private
67
+ */
68
+ const OPENAPI_TYPES = new Set(['string', 'integer', 'number', 'boolean', 'array', 'object']);
69
+
70
+ /**
71
+ * Normalise a single type definition into an OpenAPI-compatible form.
72
+ *
73
+ * - Maps unrecognised `type` values to `"string"`.
74
+ * - Recursively normalises `items` (for arrays) and `properties` (for objects).
75
+ *
76
+ * @param {Object|null|undefined} def - A definition like `{type: 'integer'}`.
77
+ * @returns {{ type: string, items?: Object, properties?: Object }} OpenAPI type object.
78
+ * @private
79
+ */
80
+ const normalise_type = (def) => {
81
+ if (!def || typeof def !== 'object') return { type: 'string' };
82
+ const t = typeof def.type === 'string' ? def.type.toLowerCase() : 'string';
83
+ const out = { type: OPENAPI_TYPES.has(t) ? t : 'string' };
84
+ if (t === 'array' && def.items) {
85
+ out.items = normalise_type(def.items);
86
+ }
87
+ if (def.properties && t === 'object') {
88
+ // Process nested properties directly to avoid infinite recursion.
89
+ // Cannot call simple_schema_to_openapi here because it would detect
90
+ // the top-level 'type' field and call normalise_type again.
91
+ const nested_props = {};
92
+ const nested_required = [];
93
+ for (const [key, prop_def] of Object.entries(def.properties)) {
94
+ if (!prop_def || typeof prop_def !== 'object') {
95
+ nested_props[key] = { type: 'string' };
96
+ continue;
97
+ }
98
+ const prop = normalise_type(prop_def);
99
+ if (prop_def.description) prop.description = String(prop_def.description);
100
+ if (prop_def.default !== undefined) prop.default = prop_def.default;
101
+ if (prop_def.enum) prop.enum = prop_def.enum;
102
+ if (prop_def.required === true) nested_required.push(key);
103
+ nested_props[key] = prop;
104
+ }
105
+ out.properties = nested_props;
106
+ if (nested_required.length) out.required = nested_required;
107
+ }
108
+ return out;
109
+ };
110
+
111
+ /**
112
+ * Convert a simple `{key: {type, description, default}}` params/returns map
113
+ * into a valid OpenAPI 3.0 Schema Object (`type: "object"` with `properties`).
114
+ *
115
+ * This is the bridge between the lightweight metadata format used in
116
+ * `server.publish()` and the verbose OpenAPI schema format.
117
+ *
118
+ * ### Supported field attributes
119
+ *
120
+ * | Attribute | Type | Description |
121
+ * |----------------|----------|------------------------------------------|
122
+ * | `type` | string | `'string'`, `'integer'`, `'number'`, `'boolean'`, `'array'`, `'object'` |
123
+ * | `description` | string | Human-readable description |
124
+ * | `default` | any | Default value |
125
+ * | `enum` | Array | List of allowed values |
126
+ * | `required` | boolean | Whether this field is required |
127
+ * | `items` | Object | For `type: 'array'`, describes each item |
128
+ * | `properties` | Object | For `type: 'object'`, nested properties |
129
+ *
130
+ * ### Pass-through behaviour
131
+ *
132
+ * If the input already looks like a raw OpenAPI schema (has a `type` string
133
+ * at the top level), it is normalised in-place rather than wrapped.
134
+ *
135
+ * @param {Object|null|undefined} schema_map - Simple schema map, or `null`.
136
+ * @returns {Object|null} An OpenAPI Schema Object, or `null` if input is falsy.
137
+ *
138
+ * @example
139
+ * // Simple params map:
140
+ * simple_schema_to_openapi({
141
+ * page: { type: 'integer', description: 'Page number', default: 1 },
142
+ * name: { type: 'string', required: true }
143
+ * });
144
+ * // → { type: 'object', properties: { page: {...}, name: {...} }, required: ['name'] }
145
+ *
146
+ * @example
147
+ * // Pass-through for pre-formed schemas:
148
+ * simple_schema_to_openapi({ type: 'array', items: { type: 'string' } });
149
+ * // → { type: 'array', items: { type: 'string' } }
150
+ */
151
+ const simple_schema_to_openapi = (schema_map) => {
152
+ if (!schema_map || typeof schema_map !== 'object') return null;
153
+
154
+ // Already looks like a raw OpenAPI schema — pass through.
155
+ if (schema_map.type && typeof schema_map.type === 'string') {
156
+ return normalise_type(schema_map);
157
+ }
158
+
159
+ const properties = {};
160
+ const required = [];
161
+ for (const [key, def] of Object.entries(schema_map)) {
162
+ if (!def || typeof def !== 'object') {
163
+ properties[key] = { type: 'string' };
164
+ continue;
165
+ }
166
+ const prop = normalise_type(def);
167
+ if (def.description) prop.description = String(def.description);
168
+ if (def.default !== undefined) prop.default = def.default;
169
+ if (def.enum) prop.enum = def.enum;
170
+ if (def.required === true) required.push(key);
171
+ properties[key] = prop;
172
+ }
173
+
174
+ const schema = { type: 'object', properties };
175
+ if (required.length) schema.required = required;
176
+ return schema;
177
+ };
178
+
179
+ // ── API Entry Collection ─────────────────────────────────────
180
+
181
+ /**
182
+ * Collect all published API entries from the server instance.
183
+ *
184
+ * Merges entries from three sources on the server, in priority order:
185
+ *
186
+ * 1. **`server._api_registry`** — richest metadata; entries added by
187
+ * `server.publish()`. These take priority if there is a key collision.
188
+ *
189
+ * 2. **`server.function_publishers`** — raw `Function_Publisher` instances.
190
+ * Each carries `name`, `api_meta`, and `schema`. Used as fallback if
191
+ * the same route is not already in `_api_registry`.
192
+ *
193
+ * 3. **`server.website_manifest.api_endpoints`** — endpoints normalised
194
+ * by `serve-factory.js` from the declarative `api:` option. Lowest
195
+ * priority since these are typically also registered via `publish()`.
196
+ *
197
+ * Deduplication key: `"METHOD /path"` (e.g. `"POST /api/users"`).
198
+ *
199
+ * @param {Object} server - JSGUI_Single_Process_Server instance.
200
+ * @returns {Array<{path: string, method: string, meta: Object, schema: Object}>}
201
+ * Array of collected API entries.
202
+ */
203
+ const collect_api_entries = (server) => {
204
+ const entries_by_key = new Map();
205
+
206
+ // Source 1: explicit registry (richest metadata).
207
+ if (Array.isArray(server._api_registry)) {
208
+ for (const entry of server._api_registry) {
209
+ const key = `${(entry.method || 'POST').toUpperCase()} ${entry.path}`;
210
+ entries_by_key.set(key, entry);
211
+ }
212
+ }
213
+
214
+ // Source 2: Function publishers (fallback if _api_registry doesn't cover them).
215
+ if (Array.isArray(server.function_publishers)) {
216
+ for (const pub of server.function_publishers) {
217
+ const name = pub.name || '';
218
+ const path = name.startsWith('/') ? name : '/api/' + name;
219
+ const method = (pub.api_meta && pub.api_meta.method) || 'POST';
220
+ const key = `${method.toUpperCase()} ${path}`;
221
+ if (!entries_by_key.has(key)) {
222
+ entries_by_key.set(key, {
223
+ path,
224
+ method: method.toUpperCase(),
225
+ meta: pub.api_meta || {},
226
+ schema: pub.schema
227
+ });
228
+ }
229
+ }
230
+ }
231
+
232
+ // Source 3: website manifest endpoints (declarative serve-factory endpoints).
233
+ if (server.website_manifest && Array.isArray(server.website_manifest.api_endpoints)) {
234
+ for (const ep of server.website_manifest.api_endpoints) {
235
+ const method = (ep.method || 'GET').toUpperCase();
236
+ const key = `${method} ${ep.path}`;
237
+ if (!entries_by_key.has(key)) {
238
+ entries_by_key.set(key, {
239
+ path: ep.path,
240
+ method,
241
+ meta: {
242
+ summary: ep.summary || ep.description || ep.name,
243
+ description: ep.description,
244
+ tags: ep.tags,
245
+ params: ep.params,
246
+ returns: ep.returns
247
+ },
248
+ schema: ep.schema
249
+ });
250
+ }
251
+ }
252
+ }
253
+
254
+ return Array.from(entries_by_key.values());
255
+ };
256
+
257
+ // ── Main Generator ───────────────────────────────────────────
258
+
259
+ /**
260
+ * Generate a complete OpenAPI 3.0.3 specification object from a jsgui3
261
+ * server instance.
262
+ *
263
+ * The returned object is a plain JSON-serialisable JavaScript object that
264
+ * conforms to the [OpenAPI 3.0.3 Specification](https://spec.openapis.org/oas/v3.0.3).
265
+ *
266
+ * ### What Gets Included
267
+ *
268
+ * - **info** — title, version, description (from `options` or server name).
269
+ * - **servers** — derived from `server.get_listening_endpoints()`.
270
+ * - **paths** — one entry per registered API endpoint (excluding the
271
+ * Swagger routes themselves: `/api/openapi.json` and `/api/docs`).
272
+ * - **tags** — auto-collected from endpoint metadata, sorted alphabetically.
273
+ * - **requestBody** — generated for POST/PUT/PATCH methods or when `params`
274
+ * metadata is present.
275
+ * - **responses** — `200` (with schema from `returns` metadata) and `500`.
276
+ *
277
+ * ### Tag Auto-Detection
278
+ *
279
+ * When an endpoint has no explicit `tags`, a tag is guessed from the route
280
+ * path. For example, `/api/users/list` produces tag `"users"`.
281
+ *
282
+ * @param {Object} server - JSGUI_Single_Process_Server instance.
283
+ * @param {Object} [options] - Override options.
284
+ * @param {string} [options.title] - Override API title (default: server.name).
285
+ * @param {string} [options.version] - Override API version (default: '1.0.0').
286
+ * @param {string} [options.description] - Override API description.
287
+ * @returns {Object} A valid OpenAPI 3.0.3 specification object.
288
+ *
289
+ * @example
290
+ * const { generate_openapi_spec } = require('./openapi');
291
+ * const spec = generate_openapi_spec(server, { title: 'My API', version: '2.0.0' });
292
+ * console.log(JSON.stringify(spec, null, 2));
293
+ */
294
+ const generate_openapi_spec = (server, options = {}) => {
295
+ const title = options.title || server.name || 'jsgui3 API';
296
+ const version = options.version || '1.0.0';
297
+ const description = options.description || `Auto-generated API documentation for ${title}`;
298
+
299
+ // Server URLs — derived from the actual listening endpoints.
300
+ const server_urls = [];
301
+ if (typeof server.get_listening_endpoints === 'function') {
302
+ const endpoints = server.get_listening_endpoints();
303
+ for (const ep of endpoints) {
304
+ server_urls.push({ url: ep.url, description: `${ep.protocol}://${ep.host}:${ep.port}` });
305
+ }
306
+ }
307
+ if (!server_urls.length) {
308
+ server_urls.push({ url: '/', description: 'Current server' });
309
+ }
310
+
311
+ // Build OpenAPI paths from collected API entries.
312
+ const paths = {};
313
+ const all_tags = new Set();
314
+ const api_entries = collect_api_entries(server);
315
+
316
+ for (const entry of api_entries) {
317
+ const { path, method } = entry;
318
+ const meta = entry.meta || {};
319
+ const method_lower = method.toLowerCase();
320
+
321
+ // Skip swagger's own routes to avoid self-referential documentation.
322
+ if (path === '/api/openapi.json' || path === '/api/docs') continue;
323
+
324
+ // ── Path parameter support ──
325
+ // Convert Express-style :param to OpenAPI {param} format.
326
+ const openapi_path = path.replace(/:([a-zA-Z_]\w*)/g, '{$1}');
327
+
328
+ // Extract path parameters from :param patterns.
329
+ const path_params = [];
330
+ const param_re = /:([a-zA-Z_]\w*)/g;
331
+ let param_match;
332
+ while ((param_match = param_re.exec(path)) !== null) {
333
+ const param_def = (meta.params && meta.params[param_match[1]]) || {};
334
+ path_params.push({
335
+ name: param_match[1],
336
+ in: 'path',
337
+ required: true,
338
+ description: param_def.description || '',
339
+ schema: { type: normalise_type(param_def).type || 'string' }
340
+ });
341
+ }
342
+
343
+ if (!paths[openapi_path]) paths[openapi_path] = {};
344
+
345
+ // --- Build the Operation Object ---
346
+ const operation = {};
347
+
348
+ // operationId: defaults to a sanitised version of the path.
349
+ operation.operationId = meta.operationId || path.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^_+|_+$/g, '');
350
+
351
+ if (meta.summary) operation.summary = meta.summary;
352
+ if (meta.description) operation.description = meta.description;
353
+ if (meta.deprecated) operation.deprecated = true;
354
+
355
+ // Tags — use explicit tags or auto-detect from route path.
356
+ const tags = meta.tags || [guess_tag(path)];
357
+ operation.tags = tags;
358
+ tags.forEach(t => all_tags.add(t));
359
+
360
+ // ── Parameters vs requestBody ──
361
+ const is_body_method = ['post', 'put', 'patch'].includes(method_lower);
362
+
363
+ if (meta.params && !is_body_method) {
364
+ // GET/HEAD/DELETE: emit params as query parameters (not requestBody).
365
+ const query_params = Object.entries(meta.params)
366
+ .filter(([name]) => !path_params.some(pp => pp.name === name))
367
+ .map(([name, def]) => {
368
+ if (!def || typeof def !== 'object') def = {};
369
+ const norm = normalise_type(def);
370
+ const param = {
371
+ name,
372
+ in: 'query',
373
+ required: !!def.required,
374
+ schema: { type: norm.type || 'string' }
375
+ };
376
+ if (def.description) param.description = def.description;
377
+ if (def.default !== undefined) param.schema.default = def.default;
378
+ if (def.enum) param.schema.enum = def.enum;
379
+ return param;
380
+ });
381
+ operation.parameters = [...path_params, ...query_params];
382
+ } else if (is_body_method || entry.schema || meta.params) {
383
+ // POST/PUT/PATCH: emit as requestBody (original behaviour).
384
+ const param_schema = meta.params
385
+ ? simple_schema_to_openapi(meta.params)
386
+ : (entry.schema && Object.keys(entry.schema).length
387
+ ? simple_schema_to_openapi(entry.schema)
388
+ : { type: 'object' });
389
+
390
+ operation.requestBody = {
391
+ required: false,
392
+ content: {
393
+ 'application/json': {
394
+ schema: param_schema || { type: 'object' }
395
+ }
396
+ }
397
+ };
398
+ // Still include path parameters if present.
399
+ if (path_params.length) {
400
+ operation.parameters = path_params;
401
+ }
402
+ } else if (path_params.length) {
403
+ // No meta.params but has path parameters.
404
+ operation.parameters = path_params;
405
+ }
406
+
407
+ // Responses — 200 success and 500 error.
408
+ const response_schema = meta.returns
409
+ ? simple_schema_to_openapi(meta.returns)
410
+ : null;
411
+
412
+ operation.responses = {
413
+ '200': {
414
+ description: meta.response_description || 'Successful response',
415
+ content: {
416
+ 'application/json': {
417
+ schema: response_schema || { type: 'object' }
418
+ }
419
+ }
420
+ },
421
+ '500': {
422
+ description: 'Internal server error',
423
+ content: {
424
+ 'application/json': {
425
+ schema: {
426
+ type: 'object',
427
+ properties: {
428
+ error: { type: 'string' }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ };
435
+
436
+ paths[openapi_path][method_lower] = operation;
437
+ }
438
+
439
+ // --- Assemble the full spec ---
440
+ const spec = {
441
+ openapi: '3.0.3',
442
+ info: {
443
+ title,
444
+ version,
445
+ description
446
+ },
447
+ servers: server_urls,
448
+ paths,
449
+ tags: Array.from(all_tags).sort().map(name => ({ name }))
450
+ };
451
+
452
+ return spec;
453
+ };
454
+
455
+ /**
456
+ * Derive a tag name from a route path for auto-grouping in Swagger UI.
457
+ *
458
+ * Strips the `/api/` prefix and uses the first path segment.
459
+ *
460
+ * @param {string} path - Route path (e.g. `/api/users/list`).
461
+ * @returns {string} Tag name (e.g. `"users"`).
462
+ * @private
463
+ *
464
+ * @example
465
+ * guess_tag('/api/users/list'); // → "users"
466
+ * guess_tag('/api/data/products'); // → "data"
467
+ * guess_tag('/health'); // → "health"
468
+ */
469
+ const guess_tag = (path) => {
470
+ const parts = path.replace(/^\/api\//, '').split('/').filter(Boolean);
471
+ return parts[0] || 'default';
472
+ };
473
+
474
+ module.exports = { generate_openapi_spec, collect_api_entries, simple_schema_to_openapi };
package/package.json CHANGED
@@ -3,28 +3,29 @@
3
3
  "main": "module.js",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
- "@babel/core": "^7.28.5",
7
- "@babel/generator": "^7.28.5",
8
- "@babel/parser": "^7.28.5",
6
+ "@babel/core": "^7.29.0",
7
+ "@babel/generator": "^7.29.0",
8
+ "@babel/parser": "^7.29.0",
9
9
  "cookies": "^0.9.1",
10
10
  "esbuild": "^0.27.1",
11
11
  "fnl": "^0.0.37",
12
12
  "fnlfs": "^0.0.34",
13
13
  "jsgui3-client": "^0.0.129",
14
- "jsgui3-html": "^0.0.180",
14
+ "jsgui3-html": "^0.0.186",
15
15
  "jsgui3-webpage": "^0.0.8",
16
16
  "jsgui3-website": "^0.0.8",
17
- "lang-tools": "^0.0.44",
17
+ "lang-tools": "^0.0.45",
18
18
  "mocha": "^11.7.5",
19
19
  "multiparty": "^4.2.3",
20
20
  "ncp": "^2.0.0",
21
- "obext": "^0.0.33",
21
+ "obext": "^0.0.34",
22
22
  "rimraf": "^6.1.2",
23
23
  "sass": "^1.77.6",
24
24
  "stream-to-array": "^2.3.0",
25
25
  "url-parse": "^1.5.10"
26
26
  },
27
27
  "devDependencies": {
28
+ "playwright": "^1.58.2",
28
29
  "puppeteer": "^19.11.1"
29
30
  },
30
31
  "commentDeps": {
@@ -43,7 +44,7 @@
43
44
  "type": "git",
44
45
  "url": "https://github.com/metabench/jsgui3-server.git"
45
46
  },
46
- "version": "0.0.151",
47
+ "version": "0.0.155",
47
48
  "scripts": {
48
49
  "cli": "node cli.js",
49
50
  "serve": "node cli.js serve",
@@ -63,10 +64,15 @@
63
64
  "test:e2e": "node tests/test-runner.js --test=end-to-end.test.js",
64
65
  "test:content": "node tests/test-runner.js --test=content-analysis.test.js",
65
66
  "test:bundler:cache": "node tests/test-runner.js --test=control-optimizer-cache-behavior.test.js",
67
+ "test:bundler:elimination:static-brackets": "node tests/test-runner.js --test=control-elimination-static-bracket-access.test.js",
68
+ "test:bundler:elimination:root-features": "node tests/test-runner.js --test=control-elimination-root-feature-pruning.test.js",
66
69
  "test:performance": "node tests/test-runner.js --test=performance.test.js",
67
70
  "test:errors": "node tests/test-runner.js --test=error-handling.test.js",
68
71
  "test:examples:controls": "node tests/test-runner.js --test=examples-controls.e2e.test.js",
72
+ "test:playwright:install": "npx playwright install chromium",
73
+ "test:playwright:smoke": "node tests/test-runner.js --test=playwright-smoke.test.js",
69
74
  "test:puppeteer:bundling": "node tests/test-runner.js --test=bundling-default-control-elimination.puppeteer.test.js",
75
+ "test:puppeteer:project-local-controls": "node tests/test-runner.js --test=project-local-controls-bundling.puppeteer.test.js",
70
76
  "test:puppeteer:windows": "node tests/test-runner.js --test=window-examples.puppeteer.test.js",
71
77
  "test:puppeteer:resources": "node tests/test-runner.js --test=window-resource-integration.puppeteer.test.js",
72
78
  "test:debug": "node tests/test-runner.js --debug",
@@ -7,11 +7,12 @@ const Publishers = {
7
7
  'html': require('./http-html-publisher'),
8
8
  'jpeg': require('./http-jpeg-publisher'),
9
9
  'js': require('./http-js-publisher'),
10
- 'observable': require('./http-observable-publisher'),
11
- 'sse': require('./http-sse-publisher'),
12
- 'png': require('./http-png-publisher'),
10
+ 'observable': require('./http-observable-publisher'),
11
+ 'sse': require('./http-sse-publisher'),
12
+ 'png': require('./http-png-publisher'),
13
13
  'resource': require('./http-resource-publisher'),
14
- 'svg': require('./http-svg-publisher')
14
+ 'svg': require('./http-svg-publisher'),
15
+ 'swagger': require('./swagger-publisher')
15
16
  }
16
17
 
17
- module.exports = Publishers;
18
+ module.exports = Publishers;