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.
- package/README.md +21 -0
- package/admin-ui/v1/controls/admin_shell.js +33 -0
- package/admin-ui/v1/server.js +14 -1
- package/docs/agi/skills/README.md +23 -0
- package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
- package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
- package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
- package/docs/agi/skills/deep-research/SKILL.md +156 -0
- package/docs/agi/skills/endurance/SKILL.md +53 -0
- package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
- package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
- package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
- package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
- package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
- package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
- package/docs/agi/skills/session-discipline/SKILL.md +40 -0
- package/docs/agi/skills/skill-writing/SKILL.md +211 -0
- package/docs/agi/skills/static-analysis/SKILL.md +58 -0
- package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
- package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
- package/docs/api-reference.md +120 -2
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/bundling-system-deep-dive.md +112 -3
- package/docs/configuration-reference.md +84 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/json-body.js +126 -0
- package/openapi.js +474 -0
- package/package.json +13 -7
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
- package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +677 -18
- package/server.js +585 -167
- package/tests/README.md +86 -2
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
- package/tests/control-elimination-root-feature-pruning.test.js +440 -0
- package/tests/control-elimination-static-bracket-access.test.js +245 -0
- package/tests/control-scan-manifest-regression.test.js +2 -0
- package/tests/end-to-end.test.js +22 -21
- package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/helpers/puppeteer-e2e-harness.js +62 -1
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- package/tests/test-runner.js +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.
|
|
7
|
-
"@babel/generator": "^7.
|
|
8
|
-
"@babel/parser": "^7.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/publishers/Publishers.js
CHANGED
|
@@ -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;
|