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.
Files changed (77) hide show
  1. package/README.md +21 -0
  2. package/admin-ui/v1/controls/admin_shell.js +33 -0
  3. package/admin-ui/v1/server.js +14 -1
  4. package/docs/api-reference.md +120 -2
  5. package/docs/books/website-design/01-introduction.md +73 -0
  6. package/docs/books/website-design/02-current-state.md +195 -0
  7. package/docs/books/website-design/03-base-class.md +181 -0
  8. package/docs/books/website-design/04-webpage.md +307 -0
  9. package/docs/books/website-design/05-website.md +456 -0
  10. package/docs/books/website-design/06-pages-storage.md +170 -0
  11. package/docs/books/website-design/07-api-layer.md +285 -0
  12. package/docs/books/website-design/08-server-integration.md +271 -0
  13. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  14. package/docs/books/website-design/10-open-questions.md +196 -0
  15. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  16. package/docs/books/website-design/12-content-model.md +395 -0
  17. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  18. package/docs/books/website-design/14-website-module-spec.md +541 -0
  19. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  20. package/docs/books/website-design/16-minimal-first.md +203 -0
  21. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  22. package/docs/books/website-design/README.md +43 -0
  23. package/docs/configuration-reference.md +54 -0
  24. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  25. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  26. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  27. package/docs/swagger.md +316 -0
  28. package/examples/controls/1) window/server.js +6 -1
  29. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  30. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  31. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  32. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  33. declarative api/e2e-screenshot-1-name-change.png +0 -0
  34. declarative api/e2e-screenshot-2-toggled.png +0 -0
  35. declarative api/e2e-screenshot-3-final.png +0 -0
  36. declarative api/e2e-screenshot-final.png +0 -0
  37. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  38. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  39. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  40. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  41. package/examples/data-views/01) query-endpoint/server.js +61 -0
  42. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  43. package/labs/website-design/002-pages-storage/check.js +244 -0
  44. package/labs/website-design/002-pages-storage/results.txt +0 -0
  45. package/labs/website-design/003-type-detection/check.js +193 -0
  46. package/labs/website-design/003-type-detection/results.txt +0 -0
  47. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  48. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  49. package/labs/website-design/005-normalize-input/check.js +303 -0
  50. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  51. package/labs/website-design/README.md +34 -0
  52. package/labs/website-design/manifest.json +68 -0
  53. package/labs/website-design/run-all.js +60 -0
  54. package/middleware/json-body.js +126 -0
  55. package/openapi.js +474 -0
  56. package/package.json +11 -8
  57. package/publishers/Publishers.js +6 -5
  58. package/publishers/http-function-publisher.js +135 -126
  59. package/publishers/http-webpage-publisher.js +89 -11
  60. package/publishers/query-publisher.js +116 -0
  61. package/publishers/swagger-publisher.js +203 -0
  62. package/publishers/swagger-ui.js +578 -0
  63. package/resources/adapters/array-adapter.js +143 -0
  64. package/resources/query-resource.js +131 -0
  65. package/serve-factory.js +728 -18
  66. package/server.js +421 -103
  67. package/tests/README.md +23 -1
  68. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  69. package/tests/helpers/playwright-e2e-harness.js +326 -0
  70. package/tests/openapi.test.js +319 -0
  71. package/tests/playwright-smoke.test.js +134 -0
  72. package/tests/publish-enhancements.test.js +673 -0
  73. package/tests/query-publisher.test.js +430 -0
  74. package/tests/quick-json-body-test.js +169 -0
  75. package/tests/serve.test.js +425 -122
  76. package/tests/swagger-publisher.test.js +1076 -0
  77. package/tests/test-runner.js +1 -0
@@ -0,0 +1,541 @@
1
+ # Chapter 14: jsgui3-website Module Specification
2
+
3
+ This chapter is a complete implementation blueprint for the `jsgui3-website` package. An agent should be able to implement the module from this spec alone.
4
+
5
+ ---
6
+
7
+ ## 14.1 Package Identity
8
+
9
+ | Field | Value |
10
+ |-------|-------|
11
+ | **npm name** | `jsgui3-website` |
12
+ | **Current version** | 0.0.8 (skeleton: `Object.assign(this, spec)`) |
13
+ | **Target version** | 0.3.0 (full-spec track; see Ch.16 for v0.1 minimal track) |
14
+ | **License** | MIT (align with current package + repo conventions) |
15
+ | **Repository** | Separate repo (same GitHub org as jsgui3-server) |
16
+
17
+ ---
18
+
19
+ ## 14.2 Dependencies
20
+
21
+ | Dependency | Type | Rationale |
22
+ |-----------|------|-----------|
23
+ | `jsgui3-html` | **production** | Provides `Evented_Class` base class |
24
+ | `jsgui3-webpage` | **production** | For creating and type-checking Webpage instances |
25
+
26
+ ### Why depend on jsgui3-webpage?
27
+
28
+ `add_page()` accepts both Webpage instances and plain specs. When given a plain spec, Website creates a Webpage internally. This ensures every page in the collection has the full Webpage API (content, i18n, finalize, etc.).
29
+
30
+ ---
31
+
32
+ ## 14.3 File Layout
33
+
34
+ ```
35
+ jsgui3-website/
36
+ ├── Website.js # Main class
37
+ ├── index.js # Re-exports Website
38
+ ├── package.json
39
+ ├── README.md # Usage documentation
40
+ ├── LICENSE
41
+ └── test/
42
+ └── website.test.js # Unit tests
43
+ ```
44
+
45
+ ### index.js
46
+
47
+ ```js
48
+ module.exports = require('./Website');
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 14.4 Constructor Signature
54
+
55
+ ```js
56
+ const site = new Website(spec);
57
+ ```
58
+
59
+ Where `spec` is an optional plain object:
60
+
61
+ ```js
62
+ {
63
+ // ── Identity ──
64
+ name: String, // Site name (e.g., 'My App')
65
+ description: String | i18n, // Site description (translatable)
66
+
67
+ // ── Configuration ──
68
+ base_path: String, // Prefix for all routes (default: undefined → server decides)
69
+ default_locale: String, // Default locale for content resolution (e.g., 'en')
70
+
71
+ // ── Collections (can also be added via methods) ──
72
+ pages: Array | Object, // Initial pages (see §14.6)
73
+ api: Object, // Initial API endpoints (see §14.8)
74
+
75
+ // ── Metadata ──
76
+ meta: Object, // Site-wide metadata
77
+ assets: Object, // Shared assets configuration
78
+ }
79
+ ```
80
+
81
+ ### Constructor behavior
82
+
83
+ 1. Store known properties with defaults
84
+ 2. Initialize `_pages` as empty `Map`
85
+ 3. Initialize `_api` as empty `Map`
86
+ 4. If `spec.pages` provided, call `add_page()` for each
87
+ 5. If `spec.api` provided, call `add_endpoint()` for each
88
+ 6. Normalize page renderer compatibility through `Webpage` rules (`ctrl` canonical, legacy `content: Function` accepted)
89
+ 7. No validation at construction time (permissive, per Ch.11)
90
+
91
+ ---
92
+
93
+ ## 14.5 Property Reference
94
+
95
+ ### Core Properties
96
+
97
+ | Property | Type | Default | Description |
98
+ |----------|------|---------|-------------|
99
+ | `name` | `string \| undefined` | `undefined` | Site name |
100
+ | `description` | `string \| object \| undefined` | `undefined` | Translatable site description |
101
+ | `base_path` | `string \| undefined` | `undefined` | URL prefix for all page paths |
102
+ | `default_locale` | `string \| undefined` | `undefined` | Default locale for content resolution |
103
+ | `meta` | `object` | `{}` | Site-wide metadata |
104
+ | `assets` | `object` | `{}` | Shared assets config |
105
+
106
+ ### Computed Properties (getters)
107
+
108
+ | Property | Type | Description |
109
+ |----------|------|-------------|
110
+ | `pages` | `Webpage[]` | Array of all pages (from Map values, insertion-order) |
111
+ | `routes` | `string[]` | Array of all page paths |
112
+ | `page_count` | `number` | Number of registered pages |
113
+ | `api_endpoints` | `object[]` | Array of all API endpoint descriptors |
114
+ | `locales` | `string[]` | Union of all locales across all pages |
115
+ | `finalized` | `boolean` | `true` after `finalize()` |
116
+
117
+ ### Private Properties
118
+
119
+ | Property | Type | Description |
120
+ |----------|------|-------------|
121
+ | `_pages` | `Map<string, Webpage>` | Page registry keyed by path |
122
+ | `_api` | `Map<string, object>` | API endpoint registry keyed by name |
123
+ | `_finalized` | `boolean` | Internal finalization flag |
124
+
125
+ ---
126
+
127
+ ## 14.6 Page Registry Methods
128
+
129
+ ### `add_page(page_or_spec) → Webpage`
130
+
131
+ Add a page to the site.
132
+
133
+ **Accepts:**
134
+ - `Webpage` instance → stored directly
135
+ - Plain object → wrapped in `new Webpage(spec)`
136
+
137
+ **Throws** `Error` if a page with the same path already exists (duplicate detection via Map, confirmed by Lab 002).
138
+
139
+ **Raises** `'page-added'` event with the Webpage as payload.
140
+
141
+ ```js
142
+ // From Webpage instance
143
+ site.add_page(new Webpage({ path: '/', ctrl: HomeCtrl }));
144
+
145
+ // From plain spec
146
+ site.add_page({ path: '/about', title: 'About', ctrl: AboutCtrl });
147
+ ```
148
+
149
+ ---
150
+
151
+ ### `get_page(path) → Webpage | undefined`
152
+
153
+ Look up a page by path. O(1) via Map.
154
+
155
+ ---
156
+
157
+ ### `has_page(path) → boolean`
158
+
159
+ Check if a page exists at the given path. O(1).
160
+
161
+ ---
162
+
163
+ ### `remove_page(path) → boolean`
164
+
165
+ Remove a page by path. Returns `true` if page existed, `false` otherwise.
166
+
167
+ **Raises** `'page-removed'` event with the removed Webpage if it existed.
168
+
169
+ ---
170
+
171
+ ### `replace_page(page_or_spec) → Webpage`
172
+
173
+ Replace an existing page at the same path. Equivalent to `remove_page(path)` + `add_page(spec)`.
174
+
175
+ **Throws** if the new spec has no path.
176
+
177
+ ---
178
+
179
+ ### Iteration
180
+
181
+ ```js
182
+ for (const page of site) {
183
+ console.log(page.path);
184
+ }
185
+ ```
186
+
187
+ Website implements `[Symbol.iterator]()` that yields pages in insertion order (delegating to Map values).
188
+
189
+ ---
190
+
191
+ ## 14.7 Page Constructor Specification
192
+
193
+ When `add_page()` receives a plain spec, it calls `_create_page(spec)`:
194
+
195
+ ```js
196
+ _create_page(spec) {
197
+ return new Webpage(spec);
198
+ }
199
+ ```
200
+
201
+ This is a protected method that subclasses can override to use a custom Webpage subclass:
202
+
203
+ ```js
204
+ class MyWebsite extends Website {
205
+ _create_page(spec) {
206
+ return new MyCustomWebpage(spec);
207
+ }
208
+ }
209
+ ```
210
+
211
+ Compatibility expectation:
212
+
213
+ 1. Website treats `Webpage.ctrl` as canonical renderer.
214
+ 2. Website accepts legacy page specs where renderer is supplied as `content: Function` (delegated to Webpage normalization).
215
+
216
+ ---
217
+
218
+ ## 14.8 API Registry Methods
219
+
220
+ ### `add_endpoint(name, handler, meta?) → this`
221
+
222
+ Register an API endpoint.
223
+
224
+ | Param | Type | Description |
225
+ |-------|------|-------------|
226
+ | `name` | `string` | Endpoint identifier |
227
+ | `handler` | `function` | Request handler |
228
+ | `meta` | `object` | Optional: `{ method, path, description }` |
229
+
230
+ **Defaults:**
231
+ - `method` → `'GET'`
232
+ - `path` → `/api/${name}`
233
+
234
+ ```js
235
+ site.add_endpoint('get-users', async (req) => {
236
+ return await db.users.list();
237
+ }, { method: 'GET', path: '/api/users' });
238
+ ```
239
+
240
+ **Throws** `Error` if an endpoint with the same name already exists.
241
+
242
+ ---
243
+
244
+ ### `get_endpoint(name) → object | undefined`
245
+
246
+ Look up an endpoint by name. Returns `{ name, handler, method, path, description }`.
247
+
248
+ ---
249
+
250
+ ### `has_endpoint(name) → boolean`
251
+
252
+ Check if an endpoint exists.
253
+
254
+ ---
255
+
256
+ ### `remove_endpoint(name) → boolean`
257
+
258
+ Remove an endpoint by name.
259
+
260
+ ---
261
+
262
+ ## 14.9 Site-Wide Content
263
+
264
+ ### `default_locale`
265
+
266
+ When pages don't specify a locale, the site's default is used:
267
+
268
+ ```js
269
+ const site = new Website({ name: 'My App', default_locale: 'en' });
270
+ ```
271
+
272
+ This is passed through to `page.resolve_content(locale)` when the server doesn't have a request-specific locale.
273
+
274
+ ### Site-level strings
275
+
276
+ Sites can have name and description as translatable values:
277
+
278
+ ```js
279
+ const site = new Website({
280
+ name: 'My App',
281
+ description: { en: 'A great app', fr: 'Une super app' }
282
+ });
283
+ ```
284
+
285
+ ---
286
+
287
+ ## 14.10 Finalize Cascade
288
+
289
+ ### `finalize() → this`
290
+
291
+ Validates the entire site and cascades to all pages:
292
+
293
+ 1. Check that at least one page exists
294
+ 2. Call `page.finalize()` for each page
295
+ 3. Collect all errors (don't stop at first)
296
+ 4. If any errors, throw a single Error with all failures listed
297
+ 5. Raise `'finalized'` event
298
+
299
+ ```js
300
+ try {
301
+ site.finalize();
302
+ } catch (e) {
303
+ // e.message includes all page-level errors:
304
+ // "Website finalization failed:
305
+ // - /about: ctrl is required
306
+ // - /blog: path is required"
307
+ }
308
+ ```
309
+
310
+ **Idempotent**: calling `finalize()` on an already-finalized site is a no-op.
311
+
312
+ ---
313
+
314
+ ## 14.11 Serialization
315
+
316
+ ### `toJSON() → object`
317
+
318
+ ```js
319
+ {
320
+ name: 'My App',
321
+ base_path: undefined,
322
+ default_locale: 'en',
323
+ page_count: 3,
324
+ routes: ['/', '/about', '/blog'],
325
+ pages: [
326
+ { path: '/', name: 'home', has_ctrl: true, locales: ['en', 'fr'] },
327
+ { path: '/about', name: 'about', has_ctrl: true, locales: ['en'] },
328
+ { path: '/blog', name: 'blog', has_ctrl: true, locales: ['en', 'fr', 'de'] }
329
+ ],
330
+ api: [
331
+ { name: 'get-users', method: 'GET', path: '/api/users' }
332
+ ],
333
+ locales: ['en', 'fr', 'de'],
334
+ finalized: false,
335
+ meta: {}
336
+ }
337
+ ```
338
+
339
+ Each page entry uses the page's `toJSON()`. The aggregate `locales` field shows the union across all pages.
340
+
341
+ ---
342
+
343
+ ## 14.12 Type Marker
344
+
345
+ ```js
346
+ get [Symbol.for('jsgui3.website')]() { return true; }
347
+ ```
348
+
349
+ Detection function:
350
+
351
+ ```js
352
+ function isWebsite(obj) {
353
+ if (obj == null || typeof obj !== 'object') return false;
354
+ if (obj[Symbol.for('jsgui3.website')] === true) return true;
355
+ return typeof obj.add_page === 'function'
356
+ && typeof obj.get_page === 'function'
357
+ && typeof obj.toJSON === 'function';
358
+ }
359
+ ```
360
+
361
+ ---
362
+
363
+ ## 14.13 Events
364
+
365
+ | Event | Payload | When |
366
+ |-------|---------|------|
367
+ | `'page-added'` | `Webpage` | After `add_page()` |
368
+ | `'page-removed'` | `Webpage` | After `remove_page()` |
369
+ | `'finalized'` | `undefined` | After `finalize()` cascade completes |
370
+
371
+ ---
372
+
373
+ ## 14.14 Validation Rules
374
+
375
+ ### Construction time
376
+
377
+ No validation — the constructor is permissive. This allows incremental composition.
378
+
379
+ ### `finalize()` time
380
+
381
+ | Condition | Behavior |
382
+ |-----------|----------|
383
+ | No pages | **throw** Error |
384
+ | Any page fails `finalize()` | collect error, **throw** aggregate |
385
+ | Duplicate paths | impossible (Map rejects at `add_page` time) |
386
+
387
+ ---
388
+
389
+ ## 14.15 Constructor Shorthand Formats
390
+
391
+ The constructor accepts pages in multiple formats:
392
+
393
+ ### Array of specs
394
+
395
+ ```js
396
+ new Website({
397
+ pages: [
398
+ { path: '/', ctrl: HomeCtrl },
399
+ { path: '/about', ctrl: AboutCtrl }
400
+ ]
401
+ });
402
+ ```
403
+
404
+ ### Object keyed by path
405
+
406
+ ```js
407
+ new Website({
408
+ pages: {
409
+ '/': { ctrl: HomeCtrl, title: 'Home' },
410
+ '/about': { ctrl: AboutCtrl, title: 'About' }
411
+ }
412
+ });
413
+ ```
414
+
415
+ ### Object with function values (shorthand)
416
+
417
+ ```js
418
+ new Website({
419
+ pages: {
420
+ '/': HomeCtrl,
421
+ '/about': AboutCtrl
422
+ }
423
+ });
424
+ ```
425
+
426
+ The constructor normalizes all three to `add_page()` calls. When pages is an object with function values, each key becomes the path and each value becomes the `ctrl`.
427
+
428
+ ### API shorthand
429
+
430
+ ```js
431
+ new Website({
432
+ api: {
433
+ 'get-users': () => db.users.list(),
434
+ 'get-posts': { handler: () => db.posts.list(), method: 'GET', path: '/api/posts' }
435
+ }
436
+ });
437
+ ```
438
+
439
+ Function values become the handler with method defaulting to `GET`.
440
+
441
+ ---
442
+
443
+ ## 14.16 Backward Compatibility
444
+
445
+ Same considerations as Webpage (Chapter 13 §13.11):
446
+
447
+ | Aspect | v0.0.8 | v0.3.0 |
448
+ |--------|--------|--------|
449
+ | Base class | None | `Evented_Class` |
450
+ | Properties | `Object.assign` | Explicit + Map registries |
451
+ | Methods | None | Full page/API registry |
452
+ | Validation | None | Two-stage |
453
+
454
+ **Breaking**: arbitrary spec properties no longer auto-assigned. Use `meta` for extras.
455
+
456
+ ---
457
+
458
+ ## 14.17 Usage Examples
459
+
460
+ ### Simple multi-page site
461
+
462
+ ```js
463
+ const Website = require('jsgui3-website');
464
+
465
+ const site = new Website({
466
+ name: 'My Portfolio',
467
+ pages: {
468
+ '/': HomeCtrl,
469
+ '/projects': ProjectsCtrl,
470
+ '/contact': ContactCtrl
471
+ }
472
+ });
473
+ ```
474
+
475
+ ### Rich site with content and API
476
+
477
+ ```js
478
+ const site = new Website({
479
+ name: 'My App',
480
+ default_locale: 'en',
481
+ pages: [
482
+ {
483
+ path: '/',
484
+ title: { en: 'Home', fr: 'Accueil' },
485
+ ctrl: HomeCtrl,
486
+ content: {
487
+ hero: { en: 'Build Faster', fr: 'Construisez plus vite' }
488
+ }
489
+ },
490
+ {
491
+ path: '/about',
492
+ title: { en: 'About', fr: 'À propos' },
493
+ ctrl: AboutCtrl,
494
+ content: require('./content/about.json')
495
+ }
496
+ ],
497
+ api: {
498
+ 'get-info': () => ({ version: '1.0' })
499
+ }
500
+ });
501
+
502
+ site.finalize(); // validates everything
503
+ ```
504
+
505
+ ### Incremental site building
506
+
507
+ ```js
508
+ const site = new Website({ name: 'My App' });
509
+
510
+ // Add pages over time
511
+ site.add_page({ path: '/', ctrl: HomeCtrl });
512
+ site.add_page({ path: '/about', ctrl: AboutCtrl });
513
+
514
+ // Add API
515
+ site.add_endpoint('health', () => ({ status: 'ok' }));
516
+
517
+ // Finalize when ready
518
+ site.finalize();
519
+ ```
520
+
521
+ ---
522
+
523
+ ## 14.18 Test Plan
524
+
525
+ 1. **Construction**: empty, with pages array, with pages object, with shorthand functions
526
+ 2. **add_page**: from spec, from Webpage instance, duplicate detection throws
527
+ 3. **get_page / has_page**: found, not found, O(1) performance
528
+ 4. **remove_page**: exists, doesn't exist, event fires
529
+ 5. **replace_page**: replaces correctly, size stays same
530
+ 6. **Iteration**: `for...of` yields insertion order
531
+ 7. **API**: add_endpoint, get_endpoint, has_endpoint, remove_endpoint
532
+ 8. **finalize()**: cascades to pages, collects errors, idempotent
533
+ 9. **toJSON()**: correct shape, includes page summaries and API
534
+ 10. **Type marker**: `Symbol.for('jsgui3.website')` is `true`
535
+ 11. **Computed**: `routes`, `page_count`, `locales`, `api_endpoints`
536
+ 12. **Events**: `page-added`, `page-removed`, `finalized`
537
+ 13. **Backward compatibility**: common property access patterns still work
538
+
539
+ ---
540
+
541
+ *Next: [Chapter 15](15-multi-repo-plan.md) covers the multi-repo implementation coordination plan.*