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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lab 006: Server Integration Spike
|
|
3
|
+
* Book Chapter: 08-server-integration + 11-converged-recommendation
|
|
4
|
+
*
|
|
5
|
+
* Question: Can a Website flow through the existing publisher pipeline?
|
|
6
|
+
*
|
|
7
|
+
* This experiment tests whether the existing Server.serve() can be
|
|
8
|
+
* extended to accept Website/Webpage objects without breaking legacy usage.
|
|
9
|
+
* It doesn't modify jsgui3-server — it probes the current behavior and
|
|
10
|
+
* proves the integration surface.
|
|
11
|
+
*
|
|
12
|
+
* Run: node labs/website-design/006-serve-website-spike/check.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
let pass = 0, fail = 0;
|
|
18
|
+
function check(label, ok) {
|
|
19
|
+
if (ok) { console.log(` ✅ ${label}`); pass++; }
|
|
20
|
+
else { console.log(` ❌ ${label}`); fail++; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('\n═══ 006: Server Integration Spike ═══\n');
|
|
24
|
+
|
|
25
|
+
const jsgui = require('jsgui3-html');
|
|
26
|
+
const { Evented_Class } = jsgui;
|
|
27
|
+
|
|
28
|
+
// ── Prototype Webpage (from experiments 004/005) ──────
|
|
29
|
+
class Webpage extends Evented_Class {
|
|
30
|
+
constructor(spec = {}) {
|
|
31
|
+
super();
|
|
32
|
+
this.path = typeof spec.path === 'string'
|
|
33
|
+
? (spec.path.startsWith('/') ? spec.path : '/' + spec.path)
|
|
34
|
+
: undefined;
|
|
35
|
+
this.name = spec.name || undefined;
|
|
36
|
+
this.title = spec.title || undefined;
|
|
37
|
+
this.content = spec.content || undefined;
|
|
38
|
+
this.meta = spec.meta || {};
|
|
39
|
+
this.render_mode = spec.render_mode || undefined;
|
|
40
|
+
this.client_js = spec.client_js || undefined;
|
|
41
|
+
this._finalized = false;
|
|
42
|
+
}
|
|
43
|
+
get has_content() { return this.content != null; }
|
|
44
|
+
get is_dynamic() { return typeof this.content === 'function'; }
|
|
45
|
+
finalize() {
|
|
46
|
+
if (this._finalized) return this;
|
|
47
|
+
if (!this.path) throw new Error('path required');
|
|
48
|
+
if (!this.has_content) throw new Error('content required');
|
|
49
|
+
this._finalized = true;
|
|
50
|
+
this.raise('finalized');
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
toJSON() {
|
|
54
|
+
return {
|
|
55
|
+
path: this.path, name: this.name, title: this.title,
|
|
56
|
+
has_content: this.has_content, is_dynamic: this.is_dynamic
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class Website extends Evented_Class {
|
|
62
|
+
constructor(spec = {}) {
|
|
63
|
+
super();
|
|
64
|
+
this.name = spec.name || undefined;
|
|
65
|
+
this.meta = spec.meta || {};
|
|
66
|
+
this._pages = new Map();
|
|
67
|
+
this._api = new Map();
|
|
68
|
+
}
|
|
69
|
+
add_page(spec) {
|
|
70
|
+
const page = spec instanceof Webpage ? spec : new Webpage(spec);
|
|
71
|
+
if (this._pages.has(page.path)) throw new Error(`Duplicate: ${page.path}`);
|
|
72
|
+
this._pages.set(page.path, page);
|
|
73
|
+
return page;
|
|
74
|
+
}
|
|
75
|
+
get_page(path) { return this._pages.get(path); }
|
|
76
|
+
has_page(path) { return this._pages.has(path); }
|
|
77
|
+
remove_page(path) { return this._pages.delete(path); }
|
|
78
|
+
get routes() { return [...this._pages.keys()]; }
|
|
79
|
+
get pages() { return [...this._pages.values()]; }
|
|
80
|
+
|
|
81
|
+
add_endpoint(name, handler, meta = {}) {
|
|
82
|
+
this._api.set(name, { name, handler, method: meta.method || 'GET', path: meta.path || `/api/${name}` });
|
|
83
|
+
}
|
|
84
|
+
get api_endpoints() { return [...this._api.values()]; }
|
|
85
|
+
|
|
86
|
+
finalize() {
|
|
87
|
+
const errors = [];
|
|
88
|
+
if (this._pages.size === 0) errors.push('no pages');
|
|
89
|
+
for (const [path, page] of this._pages) {
|
|
90
|
+
try { page.finalize(); } catch (e) { errors.push(`${path}: ${e.message}`); }
|
|
91
|
+
}
|
|
92
|
+
if (errors.length) throw new Error(`Website finalization:\n - ${errors.join('\n - ')}`);
|
|
93
|
+
this.raise('finalized');
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toJSON() {
|
|
98
|
+
return {
|
|
99
|
+
name: this.name,
|
|
100
|
+
pages: [...this._pages.values()].map(p => p.toJSON()),
|
|
101
|
+
api: [...this._api.entries()].map(([n, ep]) => ({ name: n, method: ep.method, path: ep.path }))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── TEST 1: Construct a realistic Website ──────────────
|
|
107
|
+
console.log('TEST 1: Realistic Website construction');
|
|
108
|
+
console.log('────────────────────────────────');
|
|
109
|
+
|
|
110
|
+
class HomeCtrl extends jsgui.Control {
|
|
111
|
+
constructor(spec) {
|
|
112
|
+
spec = spec || {};
|
|
113
|
+
spec.tagName = 'div';
|
|
114
|
+
super(spec);
|
|
115
|
+
if (!spec.el) this.compose();
|
|
116
|
+
}
|
|
117
|
+
compose() {
|
|
118
|
+
const h1 = new jsgui.Control({ context: this.context, tagName: 'h1' });
|
|
119
|
+
h1.text = 'Home Page';
|
|
120
|
+
this.add(h1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class AboutCtrl extends jsgui.Control {
|
|
125
|
+
constructor(spec) {
|
|
126
|
+
spec = spec || {};
|
|
127
|
+
spec.tagName = 'div';
|
|
128
|
+
super(spec);
|
|
129
|
+
if (!spec.el) this.compose();
|
|
130
|
+
}
|
|
131
|
+
compose() {
|
|
132
|
+
const h1 = new jsgui.Control({ context: this.context, tagName: 'h1' });
|
|
133
|
+
h1.text = 'About Page';
|
|
134
|
+
this.add(h1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const site = new Website({ name: 'Integration Test Site' });
|
|
139
|
+
site.add_page({ path: '/', title: 'Home', content: HomeCtrl });
|
|
140
|
+
site.add_page({ path: '/about', title: 'About', content: AboutCtrl });
|
|
141
|
+
site.add_endpoint('get-info', () => ({ version: '1.0' }), { method: 'GET' });
|
|
142
|
+
|
|
143
|
+
check('Website created', site.name === 'Integration Test Site');
|
|
144
|
+
check('Two pages registered', site.routes.length === 2);
|
|
145
|
+
check('One API endpoint', site.api_endpoints.length === 1);
|
|
146
|
+
|
|
147
|
+
// ── TEST 2: Finalize validates everything ──────────────
|
|
148
|
+
console.log('\nTEST 2: Finalize validates');
|
|
149
|
+
console.log('────────────────────────────────');
|
|
150
|
+
|
|
151
|
+
let threw = false;
|
|
152
|
+
try { site.finalize(); } catch (e) { threw = true; }
|
|
153
|
+
check('Valid website finalizes', !threw);
|
|
154
|
+
check('All pages finalized', site.pages.every(p => p._finalized));
|
|
155
|
+
|
|
156
|
+
// ── TEST 3: toJSON provides admin summary ──────────────
|
|
157
|
+
console.log('\nTEST 3: Admin introspection');
|
|
158
|
+
console.log('────────────────────────────────');
|
|
159
|
+
|
|
160
|
+
const summary = site.toJSON();
|
|
161
|
+
check('toJSON has name', summary.name === 'Integration Test Site');
|
|
162
|
+
check('toJSON has 2 pages', summary.pages.length === 2);
|
|
163
|
+
check('toJSON page has path', summary.pages[0].path === '/');
|
|
164
|
+
check('toJSON page has is_dynamic', summary.pages[0].is_dynamic === true);
|
|
165
|
+
check('toJSON has API', summary.api.length === 1);
|
|
166
|
+
check('toJSON API has method', summary.api[0].method === 'GET');
|
|
167
|
+
console.log(' Summary:', JSON.stringify(summary, null, 2).split('\n').slice(0, 8).join('\n') + '\n ...');
|
|
168
|
+
|
|
169
|
+
// ── TEST 4: Route generation for Server.serve() ───────
|
|
170
|
+
console.log('\nTEST 4: Route generation compatibility');
|
|
171
|
+
console.log('────────────────────────────────');
|
|
172
|
+
|
|
173
|
+
// Simulate what Server.serve() would do with these pages
|
|
174
|
+
const routes = [];
|
|
175
|
+
for (const page of site.pages) {
|
|
176
|
+
// This is what serve-factory.js currently does for each page
|
|
177
|
+
const routeConfig = {
|
|
178
|
+
route: page.path,
|
|
179
|
+
Ctrl: page.content,
|
|
180
|
+
title: page.title,
|
|
181
|
+
src_path_client_js: page.client_js || undefined,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Can we generate a valid route config?
|
|
185
|
+
check(`Route ${page.path}: has valid Ctrl`, typeof routeConfig.Ctrl === 'function');
|
|
186
|
+
check(`Route ${page.path}: has title`, typeof routeConfig.title === 'string');
|
|
187
|
+
routes.push(routeConfig);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── TEST 5: SSR rendering test ─────────────────────────
|
|
191
|
+
console.log('\nTEST 5: SSR rendering (server-side HTML)');
|
|
192
|
+
console.log('────────────────────────────────');
|
|
193
|
+
|
|
194
|
+
const context = new jsgui.Page_Context();
|
|
195
|
+
|
|
196
|
+
for (const page of site.pages) {
|
|
197
|
+
const Ctrl = page.content;
|
|
198
|
+
const instance = new Ctrl({ context });
|
|
199
|
+
const html = instance.all_html_render();
|
|
200
|
+
|
|
201
|
+
check(`SSR ${page.path}: renders HTML`, html.length > 0);
|
|
202
|
+
check(`SSR ${page.path}: contains expected tag`, html.includes('<h1'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── TEST 6: API endpoint invocation ────────────────────
|
|
206
|
+
console.log('\nTEST 6: API endpoint invocation');
|
|
207
|
+
console.log('────────────────────────────────');
|
|
208
|
+
|
|
209
|
+
for (const ep of site.api_endpoints) {
|
|
210
|
+
const result = ep.handler();
|
|
211
|
+
check(`API ${ep.name}: handler callable`, result !== undefined);
|
|
212
|
+
check(`API ${ep.name}: returns expected data`, result.version === '1.0');
|
|
213
|
+
check(`API ${ep.name}: method is GET`, ep.method === 'GET');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── TEST 7: Legacy compatibility ───────────────────────
|
|
217
|
+
console.log('\nTEST 7: Legacy input shapes still work');
|
|
218
|
+
console.log('────────────────────────────────');
|
|
219
|
+
|
|
220
|
+
// These are the existing Server.serve() patterns
|
|
221
|
+
// We're just verifying they're distinct from Website/Webpage
|
|
222
|
+
|
|
223
|
+
// Pattern 1: Ctrl constructor
|
|
224
|
+
check('Legacy: Ctrl is a function', typeof HomeCtrl === 'function');
|
|
225
|
+
check('Legacy: Ctrl is NOT a Website', !(HomeCtrl instanceof Website));
|
|
226
|
+
check('Legacy: Ctrl is NOT a Webpage', !(HomeCtrl instanceof Webpage));
|
|
227
|
+
|
|
228
|
+
// Pattern 2: Plain object with pages
|
|
229
|
+
const legacySpec = {
|
|
230
|
+
pages: { '/': { Ctrl: HomeCtrl }, '/about': { Ctrl: AboutCtrl } },
|
|
231
|
+
api: { 'info': () => ({}) }
|
|
232
|
+
};
|
|
233
|
+
check('Legacy: plain object is NOT a Website', !(legacySpec instanceof Website));
|
|
234
|
+
check('Legacy: can distinguish from Website via duck type',
|
|
235
|
+
typeof legacySpec.add_page !== 'function');
|
|
236
|
+
|
|
237
|
+
// Pattern 3: Webpage instance
|
|
238
|
+
const wp = new Webpage({ path: '/single', content: HomeCtrl });
|
|
239
|
+
check('Legacy: Webpage is distinct from Website', !(wp instanceof Website));
|
|
240
|
+
check('Legacy: Webpage is distinct from Ctrl', typeof wp !== 'function');
|
|
241
|
+
|
|
242
|
+
// ── TEST 8: Full lifecycle ─────────────────────────────
|
|
243
|
+
console.log('\nTEST 8: Full lifecycle walkthrough');
|
|
244
|
+
console.log('────────────────────────────────');
|
|
245
|
+
|
|
246
|
+
// Step 1: Define
|
|
247
|
+
const site2 = new Website({ name: 'Lifecycle Test' });
|
|
248
|
+
|
|
249
|
+
// Step 2: Compose incrementally
|
|
250
|
+
const homePage = site2.add_page({ path: '/' });
|
|
251
|
+
check('Step 2: page added without content', site2.has_page('/'));
|
|
252
|
+
|
|
253
|
+
// Step 3: Set content later (incremental composition)
|
|
254
|
+
homePage.content = HomeCtrl;
|
|
255
|
+
homePage.title = 'Lifecycle Home';
|
|
256
|
+
check('Step 3: content set later', homePage.has_content);
|
|
257
|
+
|
|
258
|
+
// Step 4: Add more pages
|
|
259
|
+
site2.add_page({ path: '/about', title: 'About', content: AboutCtrl });
|
|
260
|
+
check('Step 4: second page added', site2.routes.length === 2);
|
|
261
|
+
|
|
262
|
+
// Step 5: Add API
|
|
263
|
+
site2.add_endpoint('health', () => ({ status: 'ok' }));
|
|
264
|
+
check('Step 5: endpoint added', site2.api_endpoints.length === 1);
|
|
265
|
+
|
|
266
|
+
// Step 6: Finalize
|
|
267
|
+
threw = false;
|
|
268
|
+
try { site2.finalize(); } catch (e) { threw = true; console.log(' Error:', e.message); }
|
|
269
|
+
check('Step 6: finalize succeeds', !threw);
|
|
270
|
+
|
|
271
|
+
// Step 7: Serialize for admin
|
|
272
|
+
const json = site2.toJSON();
|
|
273
|
+
check('Step 7: full JSON summary', json.pages.length === 2 && json.api.length === 1);
|
|
274
|
+
|
|
275
|
+
// ── VERDICT ────────────────────────────────────────────
|
|
276
|
+
console.log('\n═══════════════════════════════════════════');
|
|
277
|
+
console.log(`RESULTS: ${pass} passed, ${fail} failed`);
|
|
278
|
+
|
|
279
|
+
console.log('\n📊 SUMMARY:');
|
|
280
|
+
console.log(' Website + Webpage objects construct, compose, and finalize cleanly');
|
|
281
|
+
console.log(' SSR rendering works with content constructors from pages');
|
|
282
|
+
console.log(' Route configs match what serve-factory.js expects');
|
|
283
|
+
console.log(' API endpoints are callable with correct metadata');
|
|
284
|
+
console.log(' Legacy input shapes are distinguishable from Website/Webpage');
|
|
285
|
+
console.log(' Incremental composition + finalize lifecycle works end-to-end');
|
|
286
|
+
console.log('\n✅ VERDICT: The proposed architecture integrates with existing patterns.');
|
|
287
|
+
console.log(' → Ch.8 + Ch.11 server integration approach confirmed');
|
|
288
|
+
console.log('═══════════════════════════════════════════\n');
|
|
289
|
+
|
|
290
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Website Design Lab Experiments
|
|
2
|
+
|
|
3
|
+
**Purpose**: Empirically test design decisions from `docs/books/website-design/` before implementation.
|
|
4
|
+
|
|
5
|
+
> **Rule**: Lab code is for learning and decision-making. Production code goes in the main packages.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Experiment Index
|
|
10
|
+
|
|
11
|
+
| # | Name | Book Chapter | Status | Decision |
|
|
12
|
+
|---|------|-------------|--------|----------|
|
|
13
|
+
| 001 | [Base Class Overhead](001-base-class-overhead/) | Ch.3 | proposed | Plain vs. Evented_Class |
|
|
14
|
+
| 002 | [Pages Storage](002-pages-storage/) | Ch.6 | proposed | Array vs. Map |
|
|
15
|
+
| 003 | [Type Detection](003-type-detection/) | Ch.8 | proposed | instanceof vs. duck typing |
|
|
16
|
+
| 004 | [Two-Stage Validation](004-two-stage-validation/) | Ch.11 | proposed | Construction + finalize |
|
|
17
|
+
| 005 | [Input Normalization](005-normalize-input/) | Ch.8+11 | proposed | Unified manifest shape |
|
|
18
|
+
| 006 | [Server Integration Spike](006-serve-website-spike/) | Ch.8+11 | proposed | End-to-end proof |
|
|
19
|
+
|
|
20
|
+
## Lifecycle
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
proposed → active → validated → decision-made
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Running
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Single experiment
|
|
30
|
+
node labs/website-design/001-base-class-overhead/check.js
|
|
31
|
+
|
|
32
|
+
# All experiments
|
|
33
|
+
node labs/website-design/run-all.js
|
|
34
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "001",
|
|
4
|
+
"slug": "base-class-overhead",
|
|
5
|
+
"name": "Base Class Overhead",
|
|
6
|
+
"status": "validated",
|
|
7
|
+
"chapter": "03-base-class",
|
|
8
|
+
"description": "Measure Evented_Class overhead vs plain class for Webpage/Website construction",
|
|
9
|
+
"path": "labs/website-design/001-base-class-overhead",
|
|
10
|
+
"check": "labs/website-design/001-base-class-overhead/check.js",
|
|
11
|
+
"decision": "Evented_Class — 5.2x slower but <3ms for 10k, ~100 bytes extra, events free"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "002",
|
|
15
|
+
"slug": "pages-storage",
|
|
16
|
+
"name": "Pages Storage Comparison",
|
|
17
|
+
"status": "validated",
|
|
18
|
+
"chapter": "06-pages-storage",
|
|
19
|
+
"description": "Benchmark Array vs Map vs Array+Index for page lookup, duplicate detection, and serialization",
|
|
20
|
+
"path": "labs/website-design/002-pages-storage",
|
|
21
|
+
"check": "labs/website-design/002-pages-storage/check.js",
|
|
22
|
+
"decision": "Map — O(1) lookup, built-in duplicate detection, insertion order, cleanest API"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "003",
|
|
26
|
+
"slug": "type-detection",
|
|
27
|
+
"name": "Type Detection Strategies",
|
|
28
|
+
"status": "validated",
|
|
29
|
+
"chapter": "08-server-integration",
|
|
30
|
+
"description": "Test instanceof vs duck typing vs Symbol.for() reliability across install scenarios",
|
|
31
|
+
"path": "labs/website-design/003-type-detection",
|
|
32
|
+
"check": "labs/website-design/003-type-detection/check.js",
|
|
33
|
+
"decision": "Combined — Symbol.for() fast path + duck typing fallback, zero false positives"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "004",
|
|
37
|
+
"slug": "two-stage-validation",
|
|
38
|
+
"name": "Two-Stage Validation",
|
|
39
|
+
"status": "validated",
|
|
40
|
+
"chapter": "11-converged-recommendation",
|
|
41
|
+
"description": "Verify construction-time lightweight + publish-time strict validation is practical",
|
|
42
|
+
"path": "labs/website-design/004-two-stage-validation",
|
|
43
|
+
"check": "labs/website-design/004-two-stage-validation/check.js",
|
|
44
|
+
"decision": "Confirmed — lightweight constructor + strict finalize(), cascading Website→Webpage"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "005",
|
|
48
|
+
"slug": "normalize-input",
|
|
49
|
+
"name": "Input Normalization",
|
|
50
|
+
"status": "validated",
|
|
51
|
+
"chapter": "08-server-integration",
|
|
52
|
+
"description": "Can all Server.serve() input shapes normalize to one manifest structure?",
|
|
53
|
+
"path": "labs/website-design/005-normalize-input",
|
|
54
|
+
"check": "labs/website-design/005-normalize-input/check.js",
|
|
55
|
+
"decision": "Confirmed — Control, Webpage, Website, plain-object all normalize to {pages[], api[], meta}"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "006",
|
|
59
|
+
"slug": "serve-website-spike",
|
|
60
|
+
"name": "Server Integration Spike",
|
|
61
|
+
"status": "validated",
|
|
62
|
+
"chapter": "08-server-integration",
|
|
63
|
+
"description": "End-to-end: Website flows through existing publisher pipeline",
|
|
64
|
+
"path": "labs/website-design/006-serve-website-spike",
|
|
65
|
+
"check": "labs/website-design/006-serve-website-spike/check.js",
|
|
66
|
+
"decision": "Confirmed — SSR works, route configs match serve-factory, legacy shapes distinguishable"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run all lab experiments in sequence.
|
|
3
|
+
*
|
|
4
|
+
* Usage: node labs/website-design/run-all.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const experiments = [
|
|
13
|
+
'001-base-class-overhead',
|
|
14
|
+
'002-pages-storage',
|
|
15
|
+
'003-type-detection',
|
|
16
|
+
'004-two-stage-validation',
|
|
17
|
+
'005-normalize-input',
|
|
18
|
+
'006-serve-website-spike',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
console.log('═══════════════════════════════════════════');
|
|
22
|
+
console.log(' Website Design Lab — Running All Checks');
|
|
23
|
+
console.log('═══════════════════════════════════════════\n');
|
|
24
|
+
|
|
25
|
+
let totalPass = 0, totalFail = 0;
|
|
26
|
+
|
|
27
|
+
for (const exp of experiments) {
|
|
28
|
+
const script = path.join(__dirname, exp, 'check.js');
|
|
29
|
+
const result = spawnSync('node', [script], {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
cwd: path.resolve(__dirname, '../..'),
|
|
32
|
+
timeout: 30000
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const output = result.stdout || '';
|
|
36
|
+
const resultLine = output.split('\n').find(l => l.includes('RESULTS:'));
|
|
37
|
+
const match = resultLine ? resultLine.match(/(\d+) passed, (\d+) failed/) : null;
|
|
38
|
+
|
|
39
|
+
const passed = match ? parseInt(match[1]) : 0;
|
|
40
|
+
const failed = match ? parseInt(match[2]) : 1;
|
|
41
|
+
|
|
42
|
+
totalPass += passed;
|
|
43
|
+
totalFail += failed;
|
|
44
|
+
|
|
45
|
+
const icon = failed === 0 ? '✅' : '❌';
|
|
46
|
+
console.log(`${icon} ${exp}: ${passed} passed, ${failed} failed`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('\n═══════════════════════════════════════════');
|
|
50
|
+
console.log(`TOTAL: ${totalPass} passed, ${totalFail} failed`);
|
|
51
|
+
|
|
52
|
+
if (totalFail === 0) {
|
|
53
|
+
console.log('\n✅ ALL EXPERIMENTS PASSED');
|
|
54
|
+
console.log(' All design book recommendations confirmed by evidence.');
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`\n❌ ${totalFail} CHECKS FAILED — review individual experiments`);
|
|
57
|
+
}
|
|
58
|
+
console.log('═══════════════════════════════════════════\n');
|
|
59
|
+
|
|
60
|
+
process.exit(totalFail > 0 ? 1 : 0);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON body parser middleware for jsgui3-server.
|
|
3
|
+
*
|
|
4
|
+
* Parses `application/json` request bodies and attaches the result to
|
|
5
|
+
* `req.body`. Designed for use with raw `(req, res)` handlers registered
|
|
6
|
+
* via `server.publish(name, fn, { raw: true })`.
|
|
7
|
+
*
|
|
8
|
+
* Function-wrapped routes (`HTTP_Function_Publisher`) already parse the
|
|
9
|
+
* body internally — this middleware is only needed for raw routes that
|
|
10
|
+
* receive POST/PUT/PATCH with JSON payloads.
|
|
11
|
+
*
|
|
12
|
+
* ### Usage
|
|
13
|
+
*
|
|
14
|
+
* ```js
|
|
15
|
+
* // As global middleware (all routes):
|
|
16
|
+
* const json_body = require('jsgui3-server/middleware/json-body');
|
|
17
|
+
* server.use(json_body());
|
|
18
|
+
*
|
|
19
|
+
* // With options:
|
|
20
|
+
* server.use(json_body({ limit: 1024 * 1024 })); // 1MB limit
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ### Options
|
|
24
|
+
*
|
|
25
|
+
* | Option | Type | Default | Description |
|
|
26
|
+
* |----------|----------|------------|------------------------------------------|
|
|
27
|
+
* | `limit` | number | `5242880` | Max body size in bytes (default 5MB) |
|
|
28
|
+
* | `strict` | boolean | `true` | Only parse `application/json` content-type. When `false`, always attempt JSON parse. |
|
|
29
|
+
*
|
|
30
|
+
* ### Behaviour
|
|
31
|
+
*
|
|
32
|
+
* - **GET/HEAD/DELETE** (no body expected): calls `next()` immediately.
|
|
33
|
+
* - **No Content-Type or non-JSON**: skips parsing, calls `next()` (unless `strict: false`).
|
|
34
|
+
* - **Empty body**: sets `req.body = null`, calls `next()`.
|
|
35
|
+
* - **Valid JSON**: sets `req.body` to the parsed object/array/value.
|
|
36
|
+
* - **Invalid JSON**: responds with `400 Bad Request` and a JSON error.
|
|
37
|
+
* - **Body exceeds limit**: responds with `413 Payload Too Large`.
|
|
38
|
+
* - **Already parsed** (`req.body` already set): skips, calls `next()`.
|
|
39
|
+
*
|
|
40
|
+
* @module middleware/json-body
|
|
41
|
+
* @param {Object} [options={}] - Parser options.
|
|
42
|
+
* @returns {Function} Express-style `(req, res, next)` middleware.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const DEFAULT_LIMIT = 5 * 1024 * 1024; // 5MB
|
|
48
|
+
|
|
49
|
+
const METHODS_WITHOUT_BODY = new Set(['GET', 'HEAD', 'DELETE', 'OPTIONS']);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a JSON body parser middleware.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} [options={}]
|
|
55
|
+
* @param {number} [options.limit=5242880] - Max body size in bytes.
|
|
56
|
+
* @param {boolean} [options.strict=true] - Only parse application/json.
|
|
57
|
+
* @returns {Function} Middleware `(req, res, next)`.
|
|
58
|
+
*/
|
|
59
|
+
const json_body = (options = {}) => {
|
|
60
|
+
const limit = options.limit || DEFAULT_LIMIT;
|
|
61
|
+
const strict = options.strict !== false;
|
|
62
|
+
|
|
63
|
+
return (req, res, next) => {
|
|
64
|
+
// Skip methods that shouldn't have a body.
|
|
65
|
+
if (METHODS_WITHOUT_BODY.has(req.method)) {
|
|
66
|
+
return next();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Skip if body already parsed by another middleware.
|
|
70
|
+
if (req.body !== undefined) {
|
|
71
|
+
return next();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check content-type in strict mode.
|
|
75
|
+
const content_type = req.headers['content-type'] || '';
|
|
76
|
+
if (strict && !content_type.startsWith('application/json')) {
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const chunks = [];
|
|
81
|
+
let size = 0;
|
|
82
|
+
let too_large = false;
|
|
83
|
+
|
|
84
|
+
req.on('data', (chunk) => {
|
|
85
|
+
size += chunk.length;
|
|
86
|
+
if (size > limit) {
|
|
87
|
+
too_large = true;
|
|
88
|
+
// Stop collecting chunks but let the stream drain naturally.
|
|
89
|
+
} else {
|
|
90
|
+
chunks.push(chunk);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.on('end', () => {
|
|
95
|
+
if (too_large) {
|
|
96
|
+
if (!res.headersSent) {
|
|
97
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify({ error: 'Payload Too Large', limit }));
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
104
|
+
|
|
105
|
+
if (raw.trim() === '') {
|
|
106
|
+
req.body = null;
|
|
107
|
+
return next();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
req.body = JSON.parse(raw);
|
|
112
|
+
next();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (!res.headersSent) {
|
|
115
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
116
|
+
res.end(JSON.stringify({
|
|
117
|
+
error: 'Invalid JSON',
|
|
118
|
+
message: err.message
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
module.exports = json_body;
|