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,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.*
|