jsgui3-server 0.0.151 → 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.
- 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/api-reference.md +120 -2
- 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/configuration-reference.md +54 -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 +11 -8
- 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/query-resource.js +131 -0
- package/serve-factory.js +728 -18
- package/server.js +421 -103
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -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 +1 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
# Design Proposals: jsgui3-website & jsgui3-webpage
|
|
2
|
+
|
|
3
|
+
> **Date**: 2026-02-15
|
|
4
|
+
> **Status**: Proposals for review — no implementation yet
|
|
5
|
+
> **Context**: These two sibling NPM packages are intended to provide abstract definitions of websites and webpages within the jsgui3 ecosystem. They should be useful abstractions but NOT required by `jsgui3-server`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Current State of the Repos
|
|
10
|
+
|
|
11
|
+
Both repos are near-empty skeletons. Here is the **exact, complete** code in each.
|
|
12
|
+
|
|
13
|
+
### 1.1 jsgui3-webpage
|
|
14
|
+
|
|
15
|
+
**Repository**: `github.com/metabench/jsgui3-webpage`
|
|
16
|
+
**Version**: 0.0.8
|
|
17
|
+
**Files**: 4 source files (+ `.git`, `.gitignore`)
|
|
18
|
+
|
|
19
|
+
#### `Webpage.js` — The entire module (14 lines)
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
/*
|
|
23
|
+
Probably does not need to do very much apart from hold info for the moment.
|
|
24
|
+
Could make subclasses do things like generare its specific parts from spec.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
class Webpage {
|
|
28
|
+
constructor(spec) {
|
|
29
|
+
Object.assign(this, spec);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = Webpage;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### `package.json`
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"name": "jsgui3-webpage",
|
|
41
|
+
"main": "Webpage.js",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"comments": [
|
|
44
|
+
"Just depend on jsgui3-html for the moment. This would be a website in the abstract sense.",
|
|
45
|
+
{ "jsgui3-html": "^0.0.139" }
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/metabench/jsgui3-webpage.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "James Vickers <james@metabench.com>",
|
|
53
|
+
"version": "0.0.8"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Note**: `jsgui3-html` is referenced in `comments` but is NOT in `dependencies`. There are zero runtime dependencies.
|
|
58
|
+
|
|
59
|
+
#### `README.md`
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
# jsgui3-webpage
|
|
63
|
+
A class that represents a webpage.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Summary**: A single class that does `Object.assign(this, spec)`. No dependencies, no methods, no validation.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 1.2 jsgui3-website
|
|
71
|
+
|
|
72
|
+
**Repository**: `github.com/metabench/jsgui3-website`
|
|
73
|
+
**Version**: 0.0.8
|
|
74
|
+
**Files**: 5 source files (+ `.git`, `.gitignore`)
|
|
75
|
+
|
|
76
|
+
#### `Website.js` — Main module (16 lines)
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
/*
|
|
80
|
+
Probably does not need to do very much apart from hold info for the moment.
|
|
81
|
+
Could make subclasses do things like generare its specific parts from spec.
|
|
82
|
+
*/
|
|
83
|
+
const API = require('./API');
|
|
84
|
+
|
|
85
|
+
class Website {
|
|
86
|
+
constructor(spec) {
|
|
87
|
+
Object.assign(this, spec);
|
|
88
|
+
this.api = new API({ server: this.server });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = Website;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `API.js` — API stub (15 lines, contains a bug)
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
class API {
|
|
99
|
+
constructor(spec) {
|
|
100
|
+
this.server = spec.server;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
publish(name, fn) {
|
|
104
|
+
// Need to access the appropriate resource publisher.
|
|
105
|
+
const {server} = this;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
MediaSourceHandle.exports = API
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **BUG**: Line 15 says `MediaSourceHandle.exports = API` — this is a typo for `module.exports = API` and will crash at runtime.
|
|
113
|
+
|
|
114
|
+
#### `package.json`
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"name": "jsgui3-website",
|
|
119
|
+
"main": "Website.js",
|
|
120
|
+
"license": "MIT",
|
|
121
|
+
"comments": [
|
|
122
|
+
"Just depend on jsgui3-html for the moment. This would be a website in the abstract sense.",
|
|
123
|
+
{ "jsgui3-html": "^0.0.139" }
|
|
124
|
+
],
|
|
125
|
+
"dependencies": {},
|
|
126
|
+
"repository": {
|
|
127
|
+
"type": "git",
|
|
128
|
+
"url": "https://github.com/metabench/jsgui3-website.git"
|
|
129
|
+
},
|
|
130
|
+
"author": "James Vickers <james@metabench.com>",
|
|
131
|
+
"version": "0.0.8"
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> **Note**: Same pattern — `jsgui3-html` mentioned in `comments` but zero actual dependencies.
|
|
136
|
+
|
|
137
|
+
#### `README.md`
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
# jsgui3-website
|
|
141
|
+
A class that represents a website. Also has functionality to deploy the website.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Summary**: Two classes. `Website` does `Object.assign` + creates an `API`. `API` has an empty `publish()` stub and a fatal export bug. No dependencies, no pages collection, no routing.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### 1.3 How jsgui3-server Currently Uses These
|
|
149
|
+
|
|
150
|
+
`jsgui3-server` depends on both packages (`^0.0.8`). Here's how they're currently consumed:
|
|
151
|
+
|
|
152
|
+
| Server file | What it does |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `website/website.js` | `module.exports = require('jsgui3-website')` — direct re-export (has dead `Obselete_Style_Website` class) |
|
|
155
|
+
| `website/webpage.js` | `module.exports = require('jsgui3-webpage')` — direct re-export (has dead `Obselete_Style_Webpage` class) |
|
|
156
|
+
| `server.js:354` | `new Webpage({ content: Ctrl })` — creates webpage from a control constructor |
|
|
157
|
+
| `server.js:428` | `new Website(opts_website)` — creates website when no Ctrl is provided |
|
|
158
|
+
| `http-website-publisher.js:117` | `spec.website instanceof Website` — type-checks the website |
|
|
159
|
+
| `serve-factory.js:33` | `new Webpage({ name, title, content, path })` — creates webpages from serve options |
|
|
160
|
+
|
|
161
|
+
The Webpage is used as a simple property bag. The Website is used as a placeholder that the publisher wraps.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 2. Design Goals
|
|
166
|
+
|
|
167
|
+
1. **Useful abstraction** — describe a website/webpage in the abstract, independent of how it's served
|
|
168
|
+
2. **Optional** — `jsgui3-server` must continue to work without these modules being required in user code
|
|
169
|
+
3. **Ecosystem-consistent** — follow jsgui3 patterns (obext, Collection, etc.) where it makes sense
|
|
170
|
+
4. **Inspectable** — admin/tooling can introspect the website structure
|
|
171
|
+
5. **Extensible** — subclassable for specific website types
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 3. Proposals for jsgui3-webpage
|
|
176
|
+
|
|
177
|
+
### Proposal A: Minimal — Enhanced Property Bag
|
|
178
|
+
|
|
179
|
+
Keep the current approach but add explicit property support and documentation.
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const jsgui = require('jsgui3-html');
|
|
183
|
+
|
|
184
|
+
class Webpage {
|
|
185
|
+
constructor(spec = {}) {
|
|
186
|
+
this.name = spec.name || undefined;
|
|
187
|
+
this.title = spec.title || undefined;
|
|
188
|
+
this.path = spec.path || '/';
|
|
189
|
+
this.content = spec.content || undefined;
|
|
190
|
+
this.meta = spec.meta || {};
|
|
191
|
+
this.client_js = spec.client_js || undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = Webpage;
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Advantages**:
|
|
199
|
+
- Dead simple — easy to understand, debug, and extend
|
|
200
|
+
- Minimal surface area — less to break
|
|
201
|
+
- Consistent with the current usage pattern in `serve-factory.js`
|
|
202
|
+
- Easy for the server to duck-type (just check for `.path` and `.content`)
|
|
203
|
+
- Extremely small module size
|
|
204
|
+
|
|
205
|
+
**Disadvantages**:
|
|
206
|
+
- No validation — any rubbish can be put in
|
|
207
|
+
- No computed properties or helpers
|
|
208
|
+
- No change tracking — can't observe mutations
|
|
209
|
+
- Properties are all public and mutable — no encapsulation
|
|
210
|
+
- Not using any jsgui3-html features despite depending on it
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### Proposal B: Observable Properties via obext
|
|
215
|
+
|
|
216
|
+
Use `obext` (already in the ecosystem) for getter/setter properties with potential change observation.
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
const jsgui = require('jsgui3-html');
|
|
220
|
+
const { prop, read_only } = require('obext');
|
|
221
|
+
|
|
222
|
+
class Webpage {
|
|
223
|
+
constructor(spec = {}) {
|
|
224
|
+
prop(this, 'name', spec.name);
|
|
225
|
+
prop(this, 'title', spec.title);
|
|
226
|
+
prop(this, 'path', spec.path || '/');
|
|
227
|
+
prop(this, 'content', spec.content);
|
|
228
|
+
prop(this, 'meta', spec.meta || {});
|
|
229
|
+
prop(this, 'client_js', spec.client_js);
|
|
230
|
+
prop(this, 'favicon', spec.favicon);
|
|
231
|
+
prop(this, 'scripts', spec.scripts || []);
|
|
232
|
+
prop(this, 'stylesheets', spec.stylesheets || []);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get has_content() {
|
|
236
|
+
return this.content !== undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get is_dynamic() {
|
|
240
|
+
return typeof this.content === 'function';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
toJSON() {
|
|
244
|
+
return {
|
|
245
|
+
name: this.name,
|
|
246
|
+
title: this.title,
|
|
247
|
+
path: this.path,
|
|
248
|
+
has_content: this.has_content,
|
|
249
|
+
is_dynamic: this.is_dynamic,
|
|
250
|
+
meta: this.meta
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = Webpage;
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Advantages**:
|
|
259
|
+
- Consistent with how `jsgui3-server` defines properties (uses `obext` extensively)
|
|
260
|
+
- Properties are encapsulated via getter/setter — room for future change events
|
|
261
|
+
- `toJSON()` makes it inspectable (useful for admin, debugging, serialization)
|
|
262
|
+
- Computed properties (`has_content`, `is_dynamic`) are useful for the server/publisher
|
|
263
|
+
- Adding `obext` as a dependency is trivial since `jsgui3-html` already depends on it
|
|
264
|
+
|
|
265
|
+
**Disadvantages**:
|
|
266
|
+
- More complex than a plain object
|
|
267
|
+
- `obext` dependency (though it's already transitive via `jsgui3-html`)
|
|
268
|
+
- Property interception may have slight performance overhead (negligible in practice)
|
|
269
|
+
- Might be over-engineering for what is currently a property bag
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### Proposal C: Evented — Extends jsgui3's Evented_Class
|
|
274
|
+
|
|
275
|
+
Make Webpage an evented object that can emit change events for reactive systems.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
const jsgui = require('jsgui3-html');
|
|
279
|
+
const { Evented_Class } = jsgui;
|
|
280
|
+
|
|
281
|
+
class Webpage extends Evented_Class {
|
|
282
|
+
constructor(spec = {}) {
|
|
283
|
+
super();
|
|
284
|
+
this._name = spec.name;
|
|
285
|
+
this._title = spec.title;
|
|
286
|
+
this._path = spec.path || '/';
|
|
287
|
+
this._content = spec.content;
|
|
288
|
+
this._meta = spec.meta || {};
|
|
289
|
+
this._client_js = spec.client_js;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
get name() { return this._name; }
|
|
293
|
+
set name(v) {
|
|
294
|
+
const old = this._name;
|
|
295
|
+
this._name = v;
|
|
296
|
+
this.raise('change', { field: 'name', old, value: v });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get title() { return this._title; }
|
|
300
|
+
set title(v) {
|
|
301
|
+
const old = this._title;
|
|
302
|
+
this._title = v;
|
|
303
|
+
this.raise('change', { field: 'title', old, value: v });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
get path() { return this._path; }
|
|
307
|
+
set path(v) {
|
|
308
|
+
const old = this._path;
|
|
309
|
+
this._path = v;
|
|
310
|
+
this.raise('change', { field: 'path', old, value: v });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get content() { return this._content; }
|
|
314
|
+
set content(v) {
|
|
315
|
+
const old = this._content;
|
|
316
|
+
this._content = v;
|
|
317
|
+
this.raise('change', { field: 'content', old, value: v });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get has_content() { return this._content !== undefined; }
|
|
321
|
+
get is_dynamic() { return typeof this._content === 'function'; }
|
|
322
|
+
|
|
323
|
+
toJSON() {
|
|
324
|
+
return {
|
|
325
|
+
name: this._name, title: this._title, path: this._path,
|
|
326
|
+
has_content: this.has_content, is_dynamic: this.is_dynamic,
|
|
327
|
+
meta: this._meta
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = Webpage;
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Advantages**:
|
|
336
|
+
- Fully reactive — admin UI or other systems can watch for page changes
|
|
337
|
+
- Consistent with other jsgui3 evented objects
|
|
338
|
+
- Enables live-reloading workflows (change page content → event fires → server re-bundles)
|
|
339
|
+
- `Evented_Class` is battle-tested within the ecosystem
|
|
340
|
+
|
|
341
|
+
**Disadvantages**:
|
|
342
|
+
- Significantly more code and complexity
|
|
343
|
+
- Every property setter fires events even when nobody is listening (slight overhead)
|
|
344
|
+
- Inheriting from `Evented_Class` couples this to jsgui3's event system
|
|
345
|
+
- YAGNI risk — no current use case needs change events on webpages
|
|
346
|
+
- Makes the class harder to understand for newcomers
|
|
347
|
+
- Boilerplate-heavy getter/setter pattern for every property
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 4. Proposals for jsgui3-website
|
|
352
|
+
|
|
353
|
+
### Proposal A: Minimal — Pages Array + API Object
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
const jsgui = require('jsgui3-html');
|
|
357
|
+
const Webpage = require('jsgui3-webpage');
|
|
358
|
+
|
|
359
|
+
class Website {
|
|
360
|
+
constructor(spec = {}) {
|
|
361
|
+
this.name = spec.name || undefined;
|
|
362
|
+
this.pages = [];
|
|
363
|
+
this.api = {};
|
|
364
|
+
this.meta = spec.meta || {};
|
|
365
|
+
this.assets = spec.assets || {};
|
|
366
|
+
|
|
367
|
+
// Initialize pages from spec
|
|
368
|
+
if (spec.pages) {
|
|
369
|
+
if (Array.isArray(spec.pages)) {
|
|
370
|
+
for (const p of spec.pages) {
|
|
371
|
+
this.add_page(p);
|
|
372
|
+
}
|
|
373
|
+
} else if (typeof spec.pages === 'object') {
|
|
374
|
+
// { '/': { content: Home }, '/about': { content: About } }
|
|
375
|
+
for (const [path, cfg] of Object.entries(spec.pages)) {
|
|
376
|
+
this.add_page({ path, ...cfg });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (spec.api && typeof spec.api === 'object') {
|
|
382
|
+
Object.assign(this.api, spec.api);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
add_page(page_or_spec) {
|
|
387
|
+
if (page_or_spec instanceof Webpage) {
|
|
388
|
+
this.pages.push(page_or_spec);
|
|
389
|
+
} else {
|
|
390
|
+
this.pages.push(new Webpage(page_or_spec));
|
|
391
|
+
}
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get_page(path) {
|
|
396
|
+
return this.pages.find(p => p.path === path);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
get page_count() {
|
|
400
|
+
return this.pages.length;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
toJSON() {
|
|
404
|
+
return {
|
|
405
|
+
name: this.name,
|
|
406
|
+
page_count: this.page_count,
|
|
407
|
+
pages: this.pages.map(p => p.toJSON ? p.toJSON() : { path: p.path, title: p.title }),
|
|
408
|
+
api_endpoints: Object.keys(this.api),
|
|
409
|
+
meta: this.meta
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = Website;
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Advantages**:
|
|
418
|
+
- Simple and understandable — uses standard JS arrays and objects
|
|
419
|
+
- `add_page()` accepts both `Webpage` instances and plain specs (flexible)
|
|
420
|
+
- `pages` object format mirrors `serve-factory.js`'s existing `pages` option exactly
|
|
421
|
+
- `toJSON()` provides admin/debug introspection
|
|
422
|
+
- Minimal code — easy to maintain, review, and extend
|
|
423
|
+
- No coupling to jsgui3 Collection internals
|
|
424
|
+
|
|
425
|
+
**Disadvantages**:
|
|
426
|
+
- Plain array — no built-in duplicate-path checking, no ordering semantics
|
|
427
|
+
- No event system — can't observe pages being added/removed
|
|
428
|
+
- Doesn't use `jsgui.Collection` — inconsistent with rest of ecosystem
|
|
429
|
+
- `api` is a plain object — no per-endpoint middleware or metadata
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
### Proposal B: Collection-Based — Ecosystem Consistent
|
|
434
|
+
|
|
435
|
+
Use `jsgui.Collection` for pages, matching the pattern used throughout `jsgui3-html` and `jsgui3-server`.
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
const jsgui = require('jsgui3-html');
|
|
439
|
+
const { Collection } = jsgui;
|
|
440
|
+
const Webpage = require('jsgui3-webpage');
|
|
441
|
+
|
|
442
|
+
class Website {
|
|
443
|
+
constructor(spec = {}) {
|
|
444
|
+
this.name = spec.name || undefined;
|
|
445
|
+
this.pages = new Collection();
|
|
446
|
+
this.api = {};
|
|
447
|
+
this.meta = spec.meta || {};
|
|
448
|
+
this.assets = spec.assets || {};
|
|
449
|
+
|
|
450
|
+
if (spec.pages) {
|
|
451
|
+
if (Array.isArray(spec.pages)) {
|
|
452
|
+
spec.pages.forEach(p => this.add_page(p));
|
|
453
|
+
} else if (typeof spec.pages === 'object') {
|
|
454
|
+
for (const [path, cfg] of Object.entries(spec.pages)) {
|
|
455
|
+
this.add_page({ path, ...cfg });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (spec.api && typeof spec.api === 'object') {
|
|
461
|
+
Object.assign(this.api, spec.api);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
add_page(page_or_spec) {
|
|
466
|
+
const page = page_or_spec instanceof Webpage
|
|
467
|
+
? page_or_spec
|
|
468
|
+
: new Webpage(page_or_spec);
|
|
469
|
+
this.pages.push(page);
|
|
470
|
+
return this;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
get_page(path) {
|
|
474
|
+
// Collection uses _arr internally
|
|
475
|
+
return this.pages._arr.find(p => p.path === path);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
get page_count() {
|
|
479
|
+
return this.pages.length();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
toJSON() {
|
|
483
|
+
const page_list = this.pages._arr.map(p =>
|
|
484
|
+
p.toJSON ? p.toJSON() : { path: p.path, title: p.title }
|
|
485
|
+
);
|
|
486
|
+
return {
|
|
487
|
+
name: this.name,
|
|
488
|
+
page_count: this.page_count,
|
|
489
|
+
pages: page_list,
|
|
490
|
+
api_endpoints: Object.keys(this.api),
|
|
491
|
+
meta: this.meta
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = Website;
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Advantages**:
|
|
500
|
+
- Consistent with `jsgui3-server/website/website.js` which already uses `Collection` for pages
|
|
501
|
+
- The existing `http-website-publisher.js` iterates `website.pages._arr` — this matches
|
|
502
|
+
- Collection provides `each()`, `length()`, etc. — ecosystem-native iteration
|
|
503
|
+
- Follows the pattern established in `website-group.js` (`extends Collection`)
|
|
504
|
+
|
|
505
|
+
**Disadvantages**:
|
|
506
|
+
- `Collection` has quirks — `length()` is a method not a property, requires `_arr` for array access
|
|
507
|
+
- Heavier dependency chain — Collection pulls in Data_Structures from jsgui3-html
|
|
508
|
+
- Less familiar to new developers — `pages._arr` is an internal detail leaking out
|
|
509
|
+
- Collection's API is not well-documented and could change
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### Proposal C: Rich Model — API Class + Validation + Helpers
|
|
514
|
+
|
|
515
|
+
A more feature-rich approach with a proper API class, validation, and convenience methods.
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
const jsgui = require('jsgui3-html');
|
|
519
|
+
const { Collection } = jsgui;
|
|
520
|
+
const Webpage = require('jsgui3-webpage');
|
|
521
|
+
|
|
522
|
+
class Website_API {
|
|
523
|
+
constructor() {
|
|
524
|
+
this._endpoints = new Map();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
publish(name, handler, options = {}) {
|
|
528
|
+
if (typeof handler !== 'function') {
|
|
529
|
+
throw new Error(`API endpoint "${name}" handler must be a function`);
|
|
530
|
+
}
|
|
531
|
+
this._endpoints.set(name, { handler, ...options });
|
|
532
|
+
return this;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
get(name) {
|
|
536
|
+
return this._endpoints.get(name);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
get endpoints() {
|
|
540
|
+
return [...this._endpoints.entries()].map(([name, cfg]) => ({
|
|
541
|
+
name,
|
|
542
|
+
method: cfg.method || 'GET',
|
|
543
|
+
description: cfg.description
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
[Symbol.iterator]() {
|
|
548
|
+
return this._endpoints[Symbol.iterator]();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
class Website {
|
|
553
|
+
constructor(spec = {}) {
|
|
554
|
+
this.name = spec.name || undefined;
|
|
555
|
+
this._pages = new Map(); // path → Webpage
|
|
556
|
+
this.api = new Website_API();
|
|
557
|
+
this.meta = spec.meta || {};
|
|
558
|
+
this.assets = spec.assets || {};
|
|
559
|
+
|
|
560
|
+
if (spec.pages) {
|
|
561
|
+
if (Array.isArray(spec.pages)) {
|
|
562
|
+
spec.pages.forEach(p => this.add_page(p));
|
|
563
|
+
} else if (typeof spec.pages === 'object') {
|
|
564
|
+
for (const [path, cfg] of Object.entries(spec.pages)) {
|
|
565
|
+
this.add_page({ path, ...cfg });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (spec.api && typeof spec.api === 'object') {
|
|
571
|
+
for (const [name, handler] of Object.entries(spec.api)) {
|
|
572
|
+
if (typeof handler === 'function') {
|
|
573
|
+
this.api.publish(name, handler);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
add_page(page_or_spec) {
|
|
580
|
+
const page = page_or_spec instanceof Webpage
|
|
581
|
+
? page_or_spec
|
|
582
|
+
: new Webpage(page_or_spec);
|
|
583
|
+
const path = page.path || '/';
|
|
584
|
+
if (this._pages.has(path)) {
|
|
585
|
+
throw new Error(`Duplicate page path: "${path}"`);
|
|
586
|
+
}
|
|
587
|
+
this._pages.set(path, page);
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
get_page(path) {
|
|
592
|
+
return this._pages.get(path);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
get pages() {
|
|
596
|
+
return [...this._pages.values()];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
get page_count() {
|
|
600
|
+
return this._pages.size;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
get routes() {
|
|
604
|
+
return [...this._pages.keys()];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
has_page(path) {
|
|
608
|
+
return this._pages.has(path);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
remove_page(path) {
|
|
612
|
+
return this._pages.delete(path);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
toJSON() {
|
|
616
|
+
return {
|
|
617
|
+
name: this.name,
|
|
618
|
+
page_count: this.page_count,
|
|
619
|
+
routes: this.routes,
|
|
620
|
+
pages: this.pages.map(p => p.toJSON ? p.toJSON() : { path: p.path, title: p.title }),
|
|
621
|
+
api: this.api.endpoints,
|
|
622
|
+
meta: this.meta
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
module.exports = Website;
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Advantages**:
|
|
631
|
+
- `Map`-based pages — O(1) lookup by path, duplicate detection, ordering preserved
|
|
632
|
+
- `Website_API` class — structured endpoint registration with optional metadata (`method`, `description`)
|
|
633
|
+
- Validation — throws on duplicate paths, validates handler is a function
|
|
634
|
+
- Rich query interface — `has_page()`, `remove_page()`, `routes` getter
|
|
635
|
+
- `Symbol.iterator` on API — modern JS iteration support
|
|
636
|
+
- Admin-friendly — `toJSON()` provides comprehensive introspection
|
|
637
|
+
- Ready for documentation generation (API endpoint descriptions)
|
|
638
|
+
|
|
639
|
+
**Disadvantages**:
|
|
640
|
+
- Most complex option — more code to maintain and test
|
|
641
|
+
- `Website_API` is a new class that doesn't exist elsewhere in the ecosystem
|
|
642
|
+
- `pages` getter returns a new array each call (minor allocation)
|
|
643
|
+
- Doesn't use `jsgui.Collection` — inconsistent with existing server code
|
|
644
|
+
- Duplicate path validation might be too strict (some apps want to override pages)
|
|
645
|
+
- The `remove_page()` method implies mutability that publishers might not handle well
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 5. Cross-Cutting Concerns
|
|
650
|
+
|
|
651
|
+
### 5.1 The API.js Bug
|
|
652
|
+
|
|
653
|
+
All proposals must fix `MediaSourceHandle.exports = API` → `module.exports = API` in the current `jsgui3-website/API.js`. This is a runtime crash.
|
|
654
|
+
|
|
655
|
+
### 5.2 Dependency on jsgui3-html
|
|
656
|
+
|
|
657
|
+
Both modules will depend on `jsgui3-html`. This is a significant dependency (~180 versions, many controls). The question is **how much** of it to use:
|
|
658
|
+
|
|
659
|
+
| Usage level | What it pulls in | Benefit |
|
|
660
|
+
|---|---|---|
|
|
661
|
+
| **Light** — just import, type-check controls | `require('jsgui3-html')` available for `instanceof` | Validates content is a proper control |
|
|
662
|
+
| **Medium** — use Collection, tof | Collection for pages, tof for type checking | Ecosystem consistency |
|
|
663
|
+
| **Heavy** — use Evented_Class, Data_Object | Event system, observable properties | Full reactivity |
|
|
664
|
+
|
|
665
|
+
### 5.3 Server Integration Strategy
|
|
666
|
+
|
|
667
|
+
Regardless of Webpage/Website design, the server integration is the same pattern:
|
|
668
|
+
|
|
669
|
+
```js
|
|
670
|
+
// In serve-factory.js — detect and unwrap Website/Webpage
|
|
671
|
+
const Website = require('jsgui3-website');
|
|
672
|
+
const Webpage = require('jsgui3-webpage');
|
|
673
|
+
|
|
674
|
+
if (input instanceof Website) {
|
|
675
|
+
// Iterate pages, register each via prepare_webpage_route
|
|
676
|
+
// Register API endpoints via server.publish()
|
|
677
|
+
} else if (input instanceof Webpage) {
|
|
678
|
+
// Single page shorthand
|
|
679
|
+
serve_options.ctrl = input.content;
|
|
680
|
+
serve_options.page_route = input.path;
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
This is additive — all existing `Server.serve()` call patterns remain unchanged.
|
|
685
|
+
|
|
686
|
+
### 5.4 Backward Compatibility
|
|
687
|
+
|
|
688
|
+
The current server code does:
|
|
689
|
+
- `new Webpage({ content: Ctrl })` → all proposals support this
|
|
690
|
+
- `website.pages._arr` iteration → Proposal B (Collection) supports this natively; others would need adaptation in the publisher
|
|
691
|
+
- `spec.website instanceof Website` → all proposals support this (same class)
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
## 6. Recommendation Matrix
|
|
696
|
+
|
|
697
|
+
| Criterion | Webpage A<br>(Minimal) | Webpage B<br>(obext) | Webpage C<br>(Evented) | Website A<br>(Array) | Website B<br>(Collection) | Website C<br>(Rich) |
|
|
698
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
699
|
+
| Simplicity | ★★★ | ★★☆ | ★☆☆ | ★★★ | ★★☆ | ★☆☆ |
|
|
700
|
+
| Ecosystem consistency | ★☆☆ | ★★★ | ★★★ | ★☆☆ | ★★★ | ★★☆ |
|
|
701
|
+
| Extensibility | ★★☆ | ★★★ | ★★★ | ★★☆ | ★★☆ | ★★★ |
|
|
702
|
+
| Admin/tooling support | ★☆☆ | ★★☆ | ★★★ | ★★☆ | ★★☆ | ★★★ |
|
|
703
|
+
| Backward compat | ★★★ | ★★★ | ★★☆ | ★★☆ | ★★★ | ★★☆ |
|
|
704
|
+
| Code maintainability | ★★★ | ★★☆ | ★☆☆ | ★★★ | ★★☆ | ★★☆ |
|
|
705
|
+
| Future-proofing | ★☆☆ | ★★☆ | ★★★ | ★☆☆ | ★★☆ | ★★★ |
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## 7. Possible Combinations
|
|
710
|
+
|
|
711
|
+
The Webpage and Website proposals can be mixed. Here are the most coherent pairings:
|
|
712
|
+
|
|
713
|
+
| Combo | Webpage | Website | Character |
|
|
714
|
+
|---|---|---|---|
|
|
715
|
+
| **Conservative** | A (Minimal) | A (Array) | Get it working with least code. Easy to review and extend later. |
|
|
716
|
+
| **Ecosystem-native** | B (obext) | B (Collection) | Matches existing jsgui3 patterns. Easiest server integration. |
|
|
717
|
+
| **Progressive** | B (obext) | C (Rich) | obext properties for pages, rich Map + API class for websites. Balance of simplicity and power. |
|
|
718
|
+
| **Full reactive** | C (Evented) | C (Rich) | Maximum capability. Good for admin UI, live workflows. Most code to maintain. |
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## 8. Open Design Questions
|
|
723
|
+
|
|
724
|
+
1. **Should Webpage know about client-side JS?** Today `src_path_client_js` is a server/publisher concern. Should it move to the page definition, or stay on the server side?
|
|
725
|
+
|
|
726
|
+
2. **Dynamic pages**: Should there be explicit support for pages whose content is generated per-request (user dashboards, etc.) vs. static pages that can be pre-bundled?
|
|
727
|
+
|
|
728
|
+
3. **Non-server use cases**: Could these modules be used by a static site generator or a build tool that pre-renders pages to HTML files on disk?
|
|
729
|
+
|
|
730
|
+
4. **Page ordering**: In a multi-page website, does page order matter? (For menus, navigation, sitemaps.)
|
|
731
|
+
|
|
732
|
+
5. **Sub-routes and nesting**: Should a Website support nested route groups? (e.g. `/blog/*` routed to a sub-website or different handler pattern)
|