jsgui3-server 0.0.150 → 0.0.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/instructions/copilot.instructions.md +1 -0
- package/AGENTS.md +2 -0
- package/README.md +89 -13
- package/admin-ui/v1/controls/admin_shell.js +702 -669
- package/admin-ui/v1/server.js +14 -1
- package/docs/api-reference.md +504 -306
- package/docs/books/creating-a-new-admin-ui/README.md +20 -20
- 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/comprehensive-documentation.md +220 -220
- package/docs/configuration-reference.md +281 -204
- package/docs/middleware-guide.md +236 -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/docs/system-architecture.md +24 -18
- 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/compression.js +217 -0
- package/middleware/index.js +15 -0
- package/middleware/json-body.js +126 -0
- package/module.js +3 -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 +756 -18
- package/server.js +502 -123
- 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,456 @@
|
|
|
1
|
+
# Chapter 5: Designing the Website
|
|
2
|
+
|
|
3
|
+
A `Website` is a collection of webpages plus site-wide configuration. It represents a complete website in the abstract — pages, API endpoints, assets, and metadata — without knowing how to serve any of it.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What should a Website know?
|
|
8
|
+
|
|
9
|
+
### Core properties
|
|
10
|
+
|
|
11
|
+
| Property | Type | Purpose |
|
|
12
|
+
|----------|------|---------|
|
|
13
|
+
| `name` | `string` | Human-readable site name |
|
|
14
|
+
| `pages` | varies (see Ch. 6) | The pages in this website |
|
|
15
|
+
| `api` | varies (see Ch. 7) | API endpoint definitions |
|
|
16
|
+
| `meta` | `object` | Site-wide metadata (base title, description, favicon, etc.) |
|
|
17
|
+
| `assets` | `object` | Static asset directory mappings |
|
|
18
|
+
|
|
19
|
+
### Extended properties
|
|
20
|
+
|
|
21
|
+
| Property | Type | Purpose |
|
|
22
|
+
|----------|------|---------|
|
|
23
|
+
| `base_path` | `string` | URL prefix for all routes (e.g. `/myapp`) |
|
|
24
|
+
| `middleware` | `function[]` | Request middleware specific to this site |
|
|
25
|
+
|
|
26
|
+
### The `base_path` question
|
|
27
|
+
|
|
28
|
+
Should a Website know its own base path? Two perspectives:
|
|
29
|
+
|
|
30
|
+
**Yes**: A website deployed at `/myapp` needs pages at `/myapp/`, `/myapp/about`, etc. If the Website doesn't know this, the server has to rewrite all paths. The Website is a self-contained description — it should include its mount point.
|
|
31
|
+
|
|
32
|
+
**No**: The base path is a deployment concern, not a definition concern. The same Website might be at `/` in production and `/staging/v2` in testing. The server should prefix paths at serve-time.
|
|
33
|
+
|
|
34
|
+
Both are valid. A pragmatic approach: accept `base_path` in the spec but default it to `undefined`, letting the server decide if not specified.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Approach A: Simple — Array Pages + Object API
|
|
39
|
+
|
|
40
|
+
The most straightforward design. Uses a plain array for pages and a plain object for API endpoints.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const { Evented_Class } = require('jsgui3-html');
|
|
44
|
+
const Webpage = require('jsgui3-webpage');
|
|
45
|
+
|
|
46
|
+
class Website extends Evented_Class {
|
|
47
|
+
constructor(spec = {}) {
|
|
48
|
+
super();
|
|
49
|
+
|
|
50
|
+
this.name = spec.name || undefined;
|
|
51
|
+
this.meta = spec.meta || {};
|
|
52
|
+
this.assets = spec.assets || {};
|
|
53
|
+
this.base_path = spec.base_path || undefined;
|
|
54
|
+
|
|
55
|
+
// Pages as a simple array
|
|
56
|
+
this.pages = [];
|
|
57
|
+
|
|
58
|
+
// API as a simple object
|
|
59
|
+
this.api = {};
|
|
60
|
+
|
|
61
|
+
// Initialize from spec
|
|
62
|
+
if (spec.pages) {
|
|
63
|
+
if (Array.isArray(spec.pages)) {
|
|
64
|
+
spec.pages.forEach(p => this.add_page(p));
|
|
65
|
+
} else if (typeof spec.pages === 'object') {
|
|
66
|
+
for (const [path, cfg] of Object.entries(spec.pages)) {
|
|
67
|
+
this.add_page({ path, ...cfg });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (spec.api && typeof spec.api === 'object') {
|
|
73
|
+
Object.assign(this.api, spec.api);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
add_page(page_or_spec) {
|
|
78
|
+
const page = page_or_spec instanceof Webpage
|
|
79
|
+
? page_or_spec
|
|
80
|
+
: new Webpage(page_or_spec);
|
|
81
|
+
this.pages.push(page);
|
|
82
|
+
this.raise('page-added', page);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get_page(path) {
|
|
87
|
+
return this.pages.find(p => p.path === path);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get page_count() {
|
|
91
|
+
return this.pages.length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
toJSON() {
|
|
95
|
+
return {
|
|
96
|
+
name: this.name,
|
|
97
|
+
page_count: this.page_count,
|
|
98
|
+
pages: this.pages.map(p => p.toJSON ? p.toJSON() : { path: p.path }),
|
|
99
|
+
api_endpoints: Object.keys(this.api),
|
|
100
|
+
meta: this.meta
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = Website;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Discussion
|
|
109
|
+
|
|
110
|
+
**What's good:**
|
|
111
|
+
- Immediately readable — no unfamiliar data structures
|
|
112
|
+
- `add_page()` accepts both Webpage instances and plain specs — flexible
|
|
113
|
+
- Object-map format (`{ '/': {...}, '/about': {...} }`) mirrors `serve-factory.js`'s existing `pages` option
|
|
114
|
+
- `'page-added'` event is trivial to add since we extend `Evented_Class`
|
|
115
|
+
- `toJSON()` enables admin introspection
|
|
116
|
+
|
|
117
|
+
**What's limiting:**
|
|
118
|
+
- Array pages — O(n) lookup by path. Fine for 5–20 pages, potentially slow for content-heavy sites
|
|
119
|
+
- No duplicate-path detection — you can add two pages at `/about` and only discover the collision at serve time
|
|
120
|
+
- API is a plain object — no metadata per endpoint (method, description, auth)
|
|
121
|
+
- `api` mixes handler functions with potential metadata, making iteration awkward
|
|
122
|
+
|
|
123
|
+
**Best for:** Small to medium sites where simplicity matters more than rigour.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Approach B: Map-Based Pages + Structured API
|
|
128
|
+
|
|
129
|
+
Uses `Map` for O(1) page lookup and a structured API registry.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const { Evented_Class } = require('jsgui3-html');
|
|
133
|
+
const Webpage = require('jsgui3-webpage');
|
|
134
|
+
|
|
135
|
+
class Website extends Evented_Class {
|
|
136
|
+
constructor(spec = {}) {
|
|
137
|
+
super();
|
|
138
|
+
|
|
139
|
+
this.name = spec.name || undefined;
|
|
140
|
+
this.meta = spec.meta || {};
|
|
141
|
+
this.assets = spec.assets || {};
|
|
142
|
+
this.base_path = spec.base_path || undefined;
|
|
143
|
+
|
|
144
|
+
// Pages stored by path for O(1) lookup
|
|
145
|
+
this._pages = new Map();
|
|
146
|
+
|
|
147
|
+
// API endpoints stored by name
|
|
148
|
+
this._api = new Map();
|
|
149
|
+
|
|
150
|
+
// Initialize from spec
|
|
151
|
+
if (spec.pages) {
|
|
152
|
+
if (Array.isArray(spec.pages)) {
|
|
153
|
+
spec.pages.forEach(p => this.add_page(p));
|
|
154
|
+
} else if (typeof spec.pages === 'object') {
|
|
155
|
+
for (const [path, cfg] of Object.entries(spec.pages)) {
|
|
156
|
+
this.add_page({ path, ...cfg });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (spec.api && typeof spec.api === 'object') {
|
|
162
|
+
for (const [name, handler] of Object.entries(spec.api)) {
|
|
163
|
+
this.add_endpoint(name, handler);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Pages ──
|
|
169
|
+
|
|
170
|
+
add_page(page_or_spec) {
|
|
171
|
+
const page = page_or_spec instanceof Webpage
|
|
172
|
+
? page_or_spec
|
|
173
|
+
: new Webpage(page_or_spec);
|
|
174
|
+
|
|
175
|
+
if (page.path && this._pages.has(page.path)) {
|
|
176
|
+
throw new Error(`Duplicate page path: "${page.path}"`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this._pages.set(page.path, page);
|
|
180
|
+
this.raise('page-added', page);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get_page(path) {
|
|
185
|
+
return this._pages.get(path);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
has_page(path) {
|
|
189
|
+
return this._pages.has(path);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
remove_page(path) {
|
|
193
|
+
const page = this._pages.get(path);
|
|
194
|
+
if (page) {
|
|
195
|
+
this._pages.delete(path);
|
|
196
|
+
this.raise('page-removed', page);
|
|
197
|
+
}
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get pages() {
|
|
202
|
+
return [...this._pages.values()];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get routes() {
|
|
206
|
+
return [...this._pages.keys()];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get page_count() {
|
|
210
|
+
return this._pages.size;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── API ──
|
|
214
|
+
|
|
215
|
+
add_endpoint(name, handler, options = {}) {
|
|
216
|
+
this._api.set(name, { handler, ...options });
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get_endpoint(name) {
|
|
221
|
+
return this._api.get(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get api_endpoints() {
|
|
225
|
+
return [...this._api.entries()].map(([name, cfg]) => ({
|
|
226
|
+
name,
|
|
227
|
+
method: cfg.method || 'GET',
|
|
228
|
+
handler: cfg.handler,
|
|
229
|
+
description: cfg.description
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
get page_count() {
|
|
234
|
+
return this._pages.size;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Serialization ──
|
|
238
|
+
|
|
239
|
+
toJSON() {
|
|
240
|
+
return {
|
|
241
|
+
name: this.name,
|
|
242
|
+
base_path: this.base_path,
|
|
243
|
+
page_count: this.page_count,
|
|
244
|
+
routes: this.routes,
|
|
245
|
+
pages: this.pages.map(p => p.toJSON ? p.toJSON() : { path: p.path }),
|
|
246
|
+
api_endpoints: this.api_endpoints.map(e => ({
|
|
247
|
+
name: e.name, method: e.method, description: e.description
|
|
248
|
+
})),
|
|
249
|
+
meta: this.meta
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = Website;
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Discussion
|
|
258
|
+
|
|
259
|
+
**What's good:**
|
|
260
|
+
- **Map-based pages** — O(1) lookup, insert-order preserved, duplicate detection
|
|
261
|
+
- **Structured API** — endpoints have metadata (method, description), not just bare functions
|
|
262
|
+
- **Event support** — `'page-added'` and `'page-removed'` events come naturally
|
|
263
|
+
- **Clean method API** — `add_page`, `get_page`, `has_page`, `remove_page`, `pages`, `routes`
|
|
264
|
+
- **No internal leakage** — `_pages` is a Map but the public API is clean methods. No `._arr` exposure
|
|
265
|
+
- **Admin-friendly** — `toJSON()` gives comprehensive introspection with endpoint descriptions
|
|
266
|
+
|
|
267
|
+
**What's debatable:**
|
|
268
|
+
- `pages` getter returns a new array each call — minor allocation. Could cache, but adds complexity.
|
|
269
|
+
- Duplicate detection throws an error — some use cases might want "last wins" override behavior. Could add a `{ replace: true }` option to `add_page()`.
|
|
270
|
+
- `_api` as a Map with structured objects is more complex than a plain `{ name: fn }` object. Is the metadata worth it right now?
|
|
271
|
+
- `remove_page()` implies runtime mutability. If publishers bundle pages at startup, removing a page after startup wouldn't un-serve it. This could be confusing.
|
|
272
|
+
|
|
273
|
+
**Best for:** Sites that need reliable path-based lookup, admin introspection, and structured API definitions.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Approach C: Inheritable — Subclass-Friendly Design
|
|
278
|
+
|
|
279
|
+
Rather than defining everything in the constructor, this design uses overridable methods that subclasses can customize.
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
const { Evented_Class } = require('jsgui3-html');
|
|
283
|
+
const Webpage = require('jsgui3-webpage');
|
|
284
|
+
|
|
285
|
+
class Website extends Evented_Class {
|
|
286
|
+
constructor(spec = {}) {
|
|
287
|
+
super();
|
|
288
|
+
|
|
289
|
+
this.name = spec.name || undefined;
|
|
290
|
+
this.meta = spec.meta || {};
|
|
291
|
+
this.assets = spec.assets || {};
|
|
292
|
+
this.base_path = spec.base_path || undefined;
|
|
293
|
+
|
|
294
|
+
this._pages = new Map();
|
|
295
|
+
this._api = new Map();
|
|
296
|
+
|
|
297
|
+
// Let subclasses define pages and API
|
|
298
|
+
if (spec.pages) this._init_pages(spec.pages);
|
|
299
|
+
if (spec.api) this._init_api(spec.api);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Override points ──
|
|
303
|
+
|
|
304
|
+
/** Override to customize page initialization from spec */
|
|
305
|
+
_init_pages(pages_spec) {
|
|
306
|
+
if (Array.isArray(pages_spec)) {
|
|
307
|
+
pages_spec.forEach(p => this.add_page(p));
|
|
308
|
+
} else if (typeof pages_spec === 'object') {
|
|
309
|
+
for (const [path, cfg] of Object.entries(pages_spec)) {
|
|
310
|
+
this.add_page({ path, ...cfg });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Override to customize API initialization from spec */
|
|
316
|
+
_init_api(api_spec) {
|
|
317
|
+
for (const [name, handler] of Object.entries(api_spec)) {
|
|
318
|
+
this.add_endpoint(name, handler);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Override to use a custom Webpage subclass */
|
|
323
|
+
_create_page(spec) {
|
|
324
|
+
return new Webpage(spec);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Page methods ──
|
|
328
|
+
|
|
329
|
+
add_page(page_or_spec) {
|
|
330
|
+
const page = page_or_spec instanceof Webpage
|
|
331
|
+
? page_or_spec
|
|
332
|
+
: this._create_page(page_or_spec);
|
|
333
|
+
|
|
334
|
+
this._pages.set(page.path, page);
|
|
335
|
+
this.raise('page-added', page);
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get_page(path) { return this._pages.get(path); }
|
|
340
|
+
has_page(path) { return this._pages.has(path); }
|
|
341
|
+
get pages() { return [...this._pages.values()]; }
|
|
342
|
+
get routes() { return [...this._pages.keys()]; }
|
|
343
|
+
get page_count(){ return this._pages.size; }
|
|
344
|
+
|
|
345
|
+
// ── API methods ──
|
|
346
|
+
|
|
347
|
+
add_endpoint(name, handler, options = {}) {
|
|
348
|
+
this._api.set(name, { handler, ...options });
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
get_endpoint(name) { return this._api.get(name); }
|
|
353
|
+
get api_endpoints() {
|
|
354
|
+
return [...this._api.entries()].map(([name, cfg]) => ({
|
|
355
|
+
name, handler: cfg.handler,
|
|
356
|
+
method: cfg.method || 'GET',
|
|
357
|
+
description: cfg.description
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Serialization ──
|
|
362
|
+
|
|
363
|
+
toJSON() {
|
|
364
|
+
return {
|
|
365
|
+
name: this.name,
|
|
366
|
+
base_path: this.base_path,
|
|
367
|
+
page_count: this.page_count,
|
|
368
|
+
routes: this.routes,
|
|
369
|
+
pages: this.pages.map(p => p.toJSON ? p.toJSON() : { path: p.path }),
|
|
370
|
+
api_endpoints: this.api_endpoints.map(({ name, method, description }) =>
|
|
371
|
+
({ name, method, description })),
|
|
372
|
+
meta: this.meta
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = Website;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Discussion
|
|
381
|
+
|
|
382
|
+
**What's good:**
|
|
383
|
+
- **Subclass-friendly** — `_create_page()` lets a specialized website use a specialized webpage. `_init_pages()` lets a subclass change how pages are loaded (e.g. from a database, from a file system scan, from a CMS).
|
|
384
|
+
- **Same public API** as Approach B — consumers don't know whether they're dealing with the base class or a subclass
|
|
385
|
+
- **Template Method pattern** — well-established OOP pattern for extensible frameworks
|
|
386
|
+
|
|
387
|
+
**What's debatable:**
|
|
388
|
+
- Extra indirection — `_init_pages`, `_create_page` add method calls that plain code doesn't need
|
|
389
|
+
- YAGNI risk — do we need subclass-customizable website types right now?
|
|
390
|
+
- `_create_page()` creates a coupling: the Website decides what kind of Webpage to use. Some designs would prefer the caller to always create their own Webpage and pass it in.
|
|
391
|
+
|
|
392
|
+
**Best for:** When you anticipate multiple types of websites (blog, docs site, SPA, etc.) that share the same structure but differ in how pages are created or initialized.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## The `spec.pages` Format Question
|
|
397
|
+
|
|
398
|
+
All three approaches accept pages in multiple formats. This is a design choice worth discussing:
|
|
399
|
+
|
|
400
|
+
### Format 1: Array of specs
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
new Website({
|
|
404
|
+
pages: [
|
|
405
|
+
{ path: '/', content: Home, title: 'Home' },
|
|
406
|
+
{ path: '/about', content: About, title: 'About' }
|
|
407
|
+
]
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Pro**: Ordered. Can have pages without paths (maybe?). Familiar array syntax.
|
|
412
|
+
**Con**: Verbose. Path is buried inside each object.
|
|
413
|
+
|
|
414
|
+
### Format 2: Object map
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
new Website({
|
|
418
|
+
pages: {
|
|
419
|
+
'/': { content: Home, title: 'Home' },
|
|
420
|
+
'/about': { content: About, title: 'About' }
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Pro**: Path-centric. Concise. Mirrors `serve-factory.js`'s existing format.
|
|
426
|
+
**Con**: Paths must be unique (can't have duplicate keys). No guaranteed order in older JS engines (though modern engines preserve insertion order for string keys).
|
|
427
|
+
|
|
428
|
+
### Format 3: Array of Webpage instances
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
new Website({
|
|
432
|
+
pages: [
|
|
433
|
+
new Webpage({ path: '/', content: Home, title: 'Home' }),
|
|
434
|
+
new Webpage({ path: '/about', content: About, title: 'About' })
|
|
435
|
+
]
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Pro**: Full control. Can use Webpage subclasses.
|
|
440
|
+
**Con**: Most verbose. Requires importing Webpage separately.
|
|
441
|
+
|
|
442
|
+
All three approaches support all three formats — the constructor detects which format was used and normalizes. This is the pragmatic choice: accept what people give you.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Comparison
|
|
447
|
+
|
|
448
|
+
| Criterion | A (Simple) | B (Map-based) | C (Inheritable) |
|
|
449
|
+
|---|:---:|:---:|:---:|
|
|
450
|
+
| Lines of code | ~55 | ~90 | ~85 |
|
|
451
|
+
| Page lookup speed | O(n) | O(1) | O(1) |
|
|
452
|
+
| Duplicate detection | ☆ | ★★★ | ★★ |
|
|
453
|
+
| Subclass-friendly | ★ | ★★ | ★★★ |
|
|
454
|
+
| Admin introspection | ★★ | ★★★ | ★★★ |
|
|
455
|
+
| API metadata | ☆ | ★★★ | ★★★ |
|
|
456
|
+
| Ease of understanding | ★★★ | ★★☆ | ★★☆ |
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Chapter 6: Pages Storage
|
|
2
|
+
|
|
3
|
+
How should a Website store its pages? This is a focused discussion of the data structure choice — separate from the overall Website design because it's a decision that applies regardless of other choices.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Requirements
|
|
8
|
+
|
|
9
|
+
A pages collection needs to support:
|
|
10
|
+
|
|
11
|
+
1. **Add a page** — with duplicate detection (or not)
|
|
12
|
+
2. **Get a page by path** — the most common operation
|
|
13
|
+
3. **List all pages** — for iteration, admin display, and site maps
|
|
14
|
+
4. **Count pages** — for diagnostics and admin UI
|
|
15
|
+
5. **Possibly remove a page** — for dynamic site composition
|
|
16
|
+
6. **Possibly maintain order** — for navigation menus, sitemaps
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Option 1: Plain Array
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
this.pages = [];
|
|
24
|
+
|
|
25
|
+
// Add
|
|
26
|
+
this.pages.push(page);
|
|
27
|
+
|
|
28
|
+
// Find
|
|
29
|
+
this.pages.find(p => p.path === '/about');
|
|
30
|
+
|
|
31
|
+
// List
|
|
32
|
+
this.pages.forEach(p => console.log(p.path));
|
|
33
|
+
|
|
34
|
+
// Count
|
|
35
|
+
this.pages.length;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Discussion
|
|
39
|
+
|
|
40
|
+
**Pro**: Everyone knows arrays. No learning curve, no surprises, excellent tooling support, easy to debug in console.
|
|
41
|
+
|
|
42
|
+
**Con**: O(n) lookup by path. No built-in duplicate detection. If you have 100 pages, `find` scans all of them each time. In practice, few websites have enough pages for this to matter — but it's architecturally sloppy.
|
|
43
|
+
|
|
44
|
+
**The duplicate problem**: Two `add_page({ path: '/' })` calls silently create two pages at `/`. The server would use the first (or last, depending on implementation) with no warning.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Option 2: JavaScript Map
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
this._pages = new Map();
|
|
52
|
+
|
|
53
|
+
// Add (with duplicate check)
|
|
54
|
+
if (this._pages.has(path)) throw new Error(`Duplicate: ${path}`);
|
|
55
|
+
this._pages.set(page.path, page);
|
|
56
|
+
|
|
57
|
+
// Find
|
|
58
|
+
this._pages.get('/about');
|
|
59
|
+
|
|
60
|
+
// List
|
|
61
|
+
[...this._pages.values()].forEach(p => console.log(p.path));
|
|
62
|
+
|
|
63
|
+
// Count
|
|
64
|
+
this._pages.size;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Discussion
|
|
68
|
+
|
|
69
|
+
**Pro**: O(1) lookup and duplicate detection. Preserves insertion order (guaranteed in ES2015+). `.has()`, `.get()`, `.set()`, `.delete()` are clean, well-known APIs. `Map` is a standard JavaScript structure — no framework dependency.
|
|
70
|
+
|
|
71
|
+
**Con**: Iteration requires `[...map.values()]` which allocates a new array. No built-in `.find()` or `.filter()` — you spread to an array first. The `_pages` prefix convention is needed because `pages` as a getter returns the array view.
|
|
72
|
+
|
|
73
|
+
**The mutation question**: Map supports `.delete()` which implies pages can be removed at runtime. If publishers bundle pages at startup, removing a page after publish wouldn't take effect. This could either be documented as expected behavior, or guarded against with a `finalized` flag.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Option 3: jsgui Collection
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const { Collection } = require('jsgui3-html');
|
|
81
|
+
this.pages = new Collection();
|
|
82
|
+
|
|
83
|
+
// Add
|
|
84
|
+
this.pages.push(page);
|
|
85
|
+
|
|
86
|
+
// Find (requires internal access)
|
|
87
|
+
this.pages._arr.find(p => p.path === '/about');
|
|
88
|
+
|
|
89
|
+
// List
|
|
90
|
+
this.pages.each(p => console.log(p.path));
|
|
91
|
+
|
|
92
|
+
// Count
|
|
93
|
+
this.pages.length(); // Note: method, not property
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Discussion
|
|
97
|
+
|
|
98
|
+
**Pro**: Ecosystem-consistent. The existing `website-group.js` uses `Collection`. The existing `http-website-publisher.js` iterates `website.pages._arr`. Using Collection means existing server code works without changes.
|
|
99
|
+
|
|
100
|
+
**Con**: `Collection` has API quirks:
|
|
101
|
+
- `.length()` is a method, not a property — easy to confuse with `Array.length`
|
|
102
|
+
- Getting the underlying array requires `._arr` — a private implementation detail
|
|
103
|
+
- No `.get(key)` — you must iterate to find by property
|
|
104
|
+
- The API is mostly undocumented
|
|
105
|
+
|
|
106
|
+
The OpenAI reviewer specifically called out `_arr` access as "baking internal details into public behavior, increasing fragility."
|
|
107
|
+
|
|
108
|
+
**Backward compatibility note**: The current `http-website-publisher.js` accesses `website.pages._arr`. If we use a different storage mechanism, that publisher code would need to change. However, that publisher is mostly NYI comments anyway and would need rewriting regardless.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Option 4: Plain Array + Path Index
|
|
113
|
+
|
|
114
|
+
A hybrid that keeps the simplicity of arrays but adds an index for fast lookup.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
this.pages = [];
|
|
118
|
+
this._path_index = {};
|
|
119
|
+
|
|
120
|
+
add_page(page) {
|
|
121
|
+
if (this._path_index[page.path]) {
|
|
122
|
+
throw new Error(`Duplicate path: ${page.path}`);
|
|
123
|
+
}
|
|
124
|
+
this.pages.push(page);
|
|
125
|
+
this._path_index[page.path] = page;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get_page(path) {
|
|
130
|
+
return this._path_index[path];
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Discussion
|
|
135
|
+
|
|
136
|
+
**Pro**: Array for iteration (familiar, no spread needed), object for O(1) lookup. Best of both worlds performance-wise.
|
|
137
|
+
|
|
138
|
+
**Con**: Two data structures to keep in sync. If someone modifies `this.pages` directly (push, splice), the index gets out of sync. More code, more surface for bugs.
|
|
139
|
+
|
|
140
|
+
**Mitigation**: Could make `this.pages` a read-only view (getter that returns a frozen copy), forcing all mutations through `add_page()`. But that adds complexity.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Comparison
|
|
145
|
+
|
|
146
|
+
| Criterion | Array | Map | Collection | Array + Index |
|
|
147
|
+
|---|:---:|:---:|:---:|:---:|
|
|
148
|
+
| Lookup speed | O(n) | O(1) | O(n) | O(1) |
|
|
149
|
+
| Duplicate detection | ☆ | ★★★ | ☆ | ★★★ |
|
|
150
|
+
| Familiarity | ★★★ | ★★★ | ★☆☆ | ★★☆ |
|
|
151
|
+
| Ecosystem consistency | ★☆☆ | ★☆☆ | ★★★ | ★☆☆ |
|
|
152
|
+
| API cleanliness | ★★★ | ★★☆ | ★☆☆ | ★★☆ |
|
|
153
|
+
| Iteration ease | ★★★ | ★★☆ | ★★☆ | ★★★ |
|
|
154
|
+
| Existing server compat | ★☆☆ | ★☆☆ | ★★★ | ★☆☆ |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## The Practical Reality
|
|
159
|
+
|
|
160
|
+
Most websites have 3–20 pages. At that scale, every option performs identically. The real differentiators are:
|
|
161
|
+
|
|
162
|
+
1. **Duplicate detection** — catching path collisions early prevents confusing bugs. Map and Array+Index provide this; plain Array and Collection don't.
|
|
163
|
+
|
|
164
|
+
2. **API cleanliness** — the methods available on the storage should make sense to consumers. Map's `.get()`, `.has()`, `.set()` are clear. Collection's `._arr` is an implementation leak.
|
|
165
|
+
|
|
166
|
+
3. **Iteration friendliness** — admin UIs and publishers need to iterate all pages. Arrays and the Array+Index hybrid are most natural for this. Map requires spreading.
|
|
167
|
+
|
|
168
|
+
4. **Existing compatibility** — the current publisher code uses `._arr`, but that code is mostly NYI and will be rewritten anyway. This shouldn't drive the design.
|
|
169
|
+
|
|
170
|
+
The Map option offers the best balance: fast lookup, built-in duplicate detection, standard JavaScript API, and insertion-order preservation. The `pages` getter that returns `[...this._pages.values()]` is the minor cost, and it's a pattern widely used in JavaScript.
|