@trebor/buildhtml 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/README.md +699 -203
- package/index.js +1309 -201
- package/package.json +38 -37
- package/LICENSE.txt +0 -15
package/README.md
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
|
-
````markdown
|
|
2
1
|
# BuildHTML
|
|
3
2
|
|
|
4
|
-
**
|
|
5
|
-
|
|
3
|
+
**High-performance, server-side rendering (SSR) library for Node.js.**
|
|
4
|
+
*"Build HTML at lightning speed with reactive state management."*
|
|
6
5
|
|
|
7
6
|
---
|
|
8
7
|
|
|
9
8
|
## Overview
|
|
10
9
|
|
|
11
|
-
BuildHTML is a lightweight SSR
|
|
10
|
+
BuildHTML is a lightweight SSR library for Node.js featuring object pooling, reactive state management, and CSS-in-JS capabilities. Build HTML on the server with minimal memory usage and blazing-fast performance.
|
|
12
11
|
|
|
13
|
-
- **Zero dependencies** – Only Node.js required
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
12
|
+
- **Zero dependencies** – Only Node.js required
|
|
13
|
+
- **High Performance** – Object pooling and LRU caching (1-5ms render time)
|
|
14
|
+
- **Reactive State** – Built-in state management with automatic UI updates
|
|
15
|
+
- **CSS-in-JS** – Scoped and global styling with automatic CSS generation
|
|
16
|
+
- **Security** – XSS protection, CSS sanitization, and CSP nonce support
|
|
17
|
+
- **Production Ready** – HTML minification, compression, and metrics
|
|
18
|
+
- **JSON Export** – Save/restore pages with optional obfuscation
|
|
17
19
|
|
|
18
20
|
---
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
npm install
|
|
24
|
-
````
|
|
25
|
-
|
|
26
|
-
or via GitHub:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm install github:0trebor0/buildhtml
|
|
25
|
+
npm install buildhtml
|
|
30
26
|
```
|
|
31
27
|
|
|
32
28
|
---
|
|
@@ -34,296 +30,796 @@ npm install github:0trebor0/buildhtml
|
|
|
34
30
|
## Quick Start
|
|
35
31
|
|
|
36
32
|
```javascript
|
|
37
|
-
const { Document } = require('
|
|
33
|
+
const { Document } = require('buildhtml');
|
|
38
34
|
|
|
39
|
-
// Create a
|
|
35
|
+
// Create a document
|
|
40
36
|
const doc = new Document();
|
|
37
|
+
doc.title('Counter App');
|
|
41
38
|
|
|
42
|
-
// Add
|
|
43
|
-
|
|
44
|
-
const button = doc.create('button').text('Increment');
|
|
45
|
-
button.bindState(counter, 'click', function() {
|
|
46
|
-
const id = '__STATE_ID__';
|
|
47
|
-
const el = document.getElementById(id);
|
|
48
|
-
const val = parseInt(window.state[id] || 0, 10) + 1;
|
|
49
|
-
window.state[id] = val;
|
|
50
|
-
el.textContent = val;
|
|
51
|
-
});
|
|
39
|
+
// Add global state
|
|
40
|
+
doc.state('count', 0);
|
|
52
41
|
|
|
53
|
-
//
|
|
54
|
-
doc.
|
|
42
|
+
// Create elements (automatically attached!)
|
|
43
|
+
const display = doc.create('h1');
|
|
44
|
+
display.bind('count', (val) => `Count: ${val}`);
|
|
45
|
+
|
|
46
|
+
const button = doc.create('button');
|
|
47
|
+
button.text('Increment');
|
|
48
|
+
button.on('click', () => { State.count++; });
|
|
55
49
|
|
|
56
50
|
// Render HTML
|
|
57
51
|
const html = doc.render();
|
|
58
52
|
console.log(html);
|
|
59
53
|
```
|
|
60
54
|
|
|
55
|
+
**Key Feature:** Elements created with `doc.create()` are **automatically attached** to the document. No manual attachment needed!
|
|
56
|
+
|
|
61
57
|
---
|
|
62
58
|
|
|
63
59
|
## Features
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
* **
|
|
67
|
-
* **State
|
|
68
|
-
* **
|
|
69
|
-
* **
|
|
70
|
-
* **
|
|
71
|
-
* **
|
|
61
|
+
### Core Features
|
|
62
|
+
* **Automatic Element Attachment** – `doc.create('div')` automatically adds to document
|
|
63
|
+
* **Reactive State** – `doc.state()` + `element.bind()` for automatic UI updates
|
|
64
|
+
* **Event Handling** – `.on(event, fn)` with automatic serialization
|
|
65
|
+
* **CSS-in-JS** – `.css({ color: 'red' })` with automatic scoping
|
|
66
|
+
* **Computed Values** – `.computed(fn)` for derived content
|
|
67
|
+
* **JSON Export/Import** – `doc.toJSON()` and `Document.fromJSON(json)`
|
|
68
|
+
|
|
69
|
+
### Performance Features
|
|
70
|
+
* **Object Pooling** – Reuses elements across renders
|
|
71
|
+
* **LRU Caching** – Cache rendered HTML for static pages
|
|
72
|
+
* **In-Flight Deduplication** – Concurrent requests share one render
|
|
73
|
+
* **Minification** – Automatic in production mode
|
|
74
|
+
* **Metrics** – Optional performance tracking
|
|
72
75
|
|
|
73
76
|
---
|
|
74
77
|
|
|
75
78
|
## API Guide
|
|
76
79
|
|
|
77
|
-
###
|
|
80
|
+
### Document
|
|
81
|
+
|
|
82
|
+
Create with `new Document(options)`.
|
|
83
|
+
|
|
84
|
+
#### Methods
|
|
85
|
+
|
|
86
|
+
| Method | Description |
|
|
87
|
+
|--------|-------------|
|
|
88
|
+
| `title(t)` | Set page title (auto-escaped) |
|
|
89
|
+
| `state(key, value)` | Set global reactive state |
|
|
90
|
+
| `addMeta(obj)` | Add meta tag: `{ name: 'description', content: '...' }` |
|
|
91
|
+
| `addLink(href)` | Add stylesheet link |
|
|
92
|
+
| `addStyle(css)` | Add inline CSS to `<head>` |
|
|
93
|
+
| `addScript(src)` | Add external script |
|
|
94
|
+
| `globalStyle(selector, rules)` | Add global CSS rule |
|
|
95
|
+
| `sharedClass(name, rules)` | Define reusable class |
|
|
96
|
+
| `create(tag)` | Create element (auto-attached to document) |
|
|
97
|
+
| `child(tag)` | Alias for `create(tag)` |
|
|
98
|
+
| `useFragment(fn)` | Add multiple elements via function |
|
|
99
|
+
| `oncreate(fn)` | Run function on page load |
|
|
100
|
+
| `toJSON()` | Export document structure as JSON |
|
|
101
|
+
| `render()` | Return full HTML string |
|
|
102
|
+
| `renderJSON(opts?)` | Render with embedded JSON |
|
|
103
|
+
| `save(path)` | Save rendered HTML to file |
|
|
104
|
+
|
|
105
|
+
#### renderJSON Options
|
|
78
106
|
|
|
79
107
|
```javascript
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Head,
|
|
84
|
-
CONFIG,
|
|
85
|
-
createCachedRenderer,
|
|
86
|
-
clearCache,
|
|
87
|
-
enableCompression,
|
|
88
|
-
responseCache,
|
|
89
|
-
warmupCache,
|
|
90
|
-
getCacheStats
|
|
91
|
-
} = require('@trebor/buildhtml');
|
|
92
|
-
```
|
|
108
|
+
// Default (no JSON)
|
|
109
|
+
doc.renderJSON()
|
|
110
|
+
// → window.__SCULPTOR_DATA__ = {...}
|
|
93
111
|
|
|
94
|
-
|
|
112
|
+
// With obfuscation (50% smaller!)
|
|
113
|
+
doc.renderJSON({ obfuscate: true })
|
|
114
|
+
// → window.__SCULPTOR_DATA__ = JSON.parse(_decrypt("..."))
|
|
95
115
|
|
|
96
|
-
|
|
116
|
+
// Custom variable name
|
|
117
|
+
doc.renderJSON('MY_DATA')
|
|
118
|
+
// → window.MY_DATA = {...}
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
// Both custom name and obfuscation
|
|
121
|
+
doc.renderJSON('MY_DATA', { obfuscate: true })
|
|
122
|
+
// → window.MY_DATA = JSON.parse(_decrypt("..."))
|
|
123
|
+
|
|
124
|
+
// Or use options object
|
|
125
|
+
doc.renderJSON({ obfuscate: true, varName: 'MY_DATA' })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Static Methods
|
|
99
129
|
|
|
100
130
|
| Method | Description |
|
|
101
131
|
|--------|-------------|
|
|
102
|
-
| `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| `render()` | Return full HTML string. If cache options are set, may return cached result or populate cache. Clears the document after render. |
|
|
113
|
-
|
|
114
|
-
**Constructor options**
|
|
115
|
-
|
|
116
|
-
| Option | Type | Description |
|
|
117
|
-
|--------|------|-------------|
|
|
118
|
-
| `cache` | `boolean` | If `true`, use response cache when `cacheKey` is set (e.g. by `createCachedRenderer`). |
|
|
119
|
-
| `cacheKey` | `string` | Key for response cache. |
|
|
120
|
-
|
|
121
|
-
**Example**
|
|
122
|
-
|
|
123
|
-
```javascript
|
|
124
|
-
const doc = new Document({ cache: true, cacheKey: 'home' });
|
|
125
|
-
doc.title('Home').addMeta({ charset: 'UTF-8' });
|
|
126
|
-
doc.use(doc.create('main').append(doc.create('p').text('Hello')));
|
|
127
|
-
const html = doc.render();
|
|
132
|
+
| `Document.fromJSON(json)` | Rebuild document from JSON |
|
|
133
|
+
|
|
134
|
+
#### Constructor Options
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
new Document({
|
|
138
|
+
cache: true, // Enable response caching
|
|
139
|
+
cacheKey: 'home', // Cache key for this document
|
|
140
|
+
nonce: 'abc123' // CSP nonce for inline scripts/styles
|
|
141
|
+
})
|
|
128
142
|
```
|
|
129
143
|
|
|
130
144
|
---
|
|
131
145
|
|
|
132
146
|
### Element
|
|
133
147
|
|
|
134
|
-
Created with `doc.create(tag)
|
|
148
|
+
Created with `doc.create(tag)`. All methods return `this` for chaining.
|
|
149
|
+
|
|
150
|
+
#### Methods
|
|
135
151
|
|
|
136
152
|
| Method | Description |
|
|
137
153
|
|--------|-------------|
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
154
|
+
| `create(tag)` | Create child element (auto-attached to parent) |
|
|
155
|
+
| `child(tag)` | Alias for `create(tag)` |
|
|
156
|
+
| `id(v?)` | Set id attribute (auto-generated if omitted) |
|
|
157
|
+
| `attr(key, value)` | Set attribute |
|
|
158
|
+
| `text(content)` | Append escaped text |
|
|
159
|
+
| `append(child)` | Append element or text |
|
|
160
|
+
| `appendUnsafe(html)` | Append raw HTML (use carefully!) |
|
|
161
|
+
| `css(styles)` | Add scoped styles: `{ color: 'red' }` |
|
|
162
|
+
| `uniqueClass(rules)` | Add unique class with styles |
|
|
163
|
+
| `state(value)` | Set initial state for hydration |
|
|
164
|
+
| `bind(stateKey, fn?)` | Bind to global state |
|
|
165
|
+
| `computed(fn)` | Compute content from state |
|
|
166
|
+
| `on(event, fn)` | Attach event handler |
|
|
167
|
+
|
|
168
|
+
#### Examples
|
|
146
169
|
|
|
147
|
-
|
|
170
|
+
```javascript
|
|
171
|
+
// Basic element
|
|
172
|
+
const div = doc.create('div')
|
|
173
|
+
.attr('class', 'container')
|
|
174
|
+
.text('Hello World');
|
|
175
|
+
|
|
176
|
+
// Nested elements (auto-attached to parent)
|
|
177
|
+
const container = doc.create('div');
|
|
178
|
+
container.create('h1').text('Title');
|
|
179
|
+
container.create('p').text('Content');
|
|
180
|
+
|
|
181
|
+
// CSS-in-JS
|
|
182
|
+
const box = doc.create('div').css({
|
|
183
|
+
padding: '20px',
|
|
184
|
+
backgroundColor: '#f0f0f0',
|
|
185
|
+
borderRadius: '8px'
|
|
186
|
+
});
|
|
148
187
|
|
|
149
|
-
|
|
150
|
-
|
|
188
|
+
// State binding
|
|
189
|
+
doc.state('username', 'Alice');
|
|
190
|
+
const greeting = doc.create('div');
|
|
191
|
+
greeting.bind('username', (name) => `Hello, ${name}!`);
|
|
151
192
|
|
|
152
|
-
|
|
193
|
+
// Event handling
|
|
194
|
+
const button = doc.create('button').text('Click me');
|
|
195
|
+
button.on('click', () => {
|
|
196
|
+
State.count++;
|
|
197
|
+
console.log('Clicked!');
|
|
198
|
+
});
|
|
153
199
|
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
// Computed values
|
|
201
|
+
const total = doc.create('div');
|
|
202
|
+
total.computed((state) => {
|
|
203
|
+
return state.price * state.quantity;
|
|
204
|
+
});
|
|
158
205
|
```
|
|
159
206
|
|
|
160
207
|
---
|
|
161
208
|
|
|
162
|
-
###
|
|
209
|
+
### Global State & Reactivity
|
|
163
210
|
|
|
164
|
-
|
|
211
|
+
Sculptor provides a reactive state system:
|
|
165
212
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
```javascript
|
|
214
|
+
// Set global state
|
|
215
|
+
doc.state('count', 0);
|
|
216
|
+
doc.state('user', { name: 'Alice', age: 30 });
|
|
217
|
+
|
|
218
|
+
// Bind elements to state
|
|
219
|
+
const display = doc.create('div');
|
|
220
|
+
display.bind('count'); // Shows raw value
|
|
221
|
+
|
|
222
|
+
const formatted = doc.create('div');
|
|
223
|
+
formatted.bind('count', (val) => `Count: ${val}`); // Transform
|
|
224
|
+
|
|
225
|
+
// Update state (automatically updates UI)
|
|
226
|
+
button.on('click', () => {
|
|
227
|
+
State.count++; // Global State proxy
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Access state in browser
|
|
231
|
+
// window.State.count
|
|
232
|
+
// window.State.user
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**How it works:**
|
|
236
|
+
- Server renders initial HTML
|
|
237
|
+
- Client receives `window.State` as reactive Proxy
|
|
238
|
+
- Changing `State.count++` automatically updates all bound elements
|
|
239
|
+
- No manual DOM manipulation needed!
|
|
175
240
|
|
|
176
241
|
---
|
|
177
242
|
|
|
178
|
-
###
|
|
243
|
+
### Exports
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
const {
|
|
247
|
+
Document, // Main class
|
|
248
|
+
Element, // Element class (usually not used directly)
|
|
249
|
+
Head, // Head manager (usually via doc.title(), etc.)
|
|
250
|
+
CONFIG, // Global configuration
|
|
251
|
+
createCachedRenderer, // Express middleware
|
|
252
|
+
clearCache, // Clear response cache
|
|
253
|
+
enableCompression, // Gzip middleware
|
|
254
|
+
responseCache, // LRU cache instance
|
|
255
|
+
warmupCache, // Pre-render routes
|
|
256
|
+
getCacheStats, // Cache statistics
|
|
257
|
+
resetPools, // Reset object pools
|
|
258
|
+
healthCheck, // Health check data
|
|
259
|
+
metrics // Performance metrics
|
|
260
|
+
} = require('buildhtml');
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Express Integration
|
|
266
|
+
|
|
267
|
+
### Basic Route
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
const express = require('express');
|
|
271
|
+
const { Document } = require('buildhtml');
|
|
272
|
+
|
|
273
|
+
const app = express();
|
|
274
|
+
|
|
275
|
+
app.get('/', (req, res) => {
|
|
276
|
+
const doc = new Document();
|
|
277
|
+
doc.title('Home');
|
|
278
|
+
|
|
279
|
+
doc.create('h1').text('Welcome!');
|
|
280
|
+
doc.create('p').text('Built with BuildHTML');
|
|
281
|
+
|
|
282
|
+
res.send(doc.render());
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Cached Static Page
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
const { createCachedRenderer } = require('sculptor-js');
|
|
290
|
+
|
|
291
|
+
app.get('/about', createCachedRenderer(
|
|
292
|
+
async (req) => {
|
|
293
|
+
const doc = new Document();
|
|
294
|
+
doc.title('About Us');
|
|
295
|
+
doc.create('h1').text('About');
|
|
296
|
+
return doc;
|
|
297
|
+
},
|
|
298
|
+
'about-page' // Cache key
|
|
299
|
+
));
|
|
300
|
+
|
|
301
|
+
// First request: ~3ms (render)
|
|
302
|
+
// Cached requests: <0.1ms (from cache)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Dynamic Content
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
app.get('/user/:name', async (req, res) => {
|
|
309
|
+
const doc = new Document();
|
|
310
|
+
doc.title(`Profile - ${req.params.name}`);
|
|
311
|
+
doc.state('userName', req.params.name);
|
|
312
|
+
|
|
313
|
+
const greeting = doc.create('h1');
|
|
314
|
+
greeting.bind('userName', (name) => `Welcome, ${name}!`);
|
|
315
|
+
|
|
316
|
+
res.send(doc.render());
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Interactive Counter
|
|
179
321
|
|
|
180
|
-
|
|
322
|
+
```javascript
|
|
323
|
+
app.get('/counter', (req, res) => {
|
|
324
|
+
const doc = new Document();
|
|
325
|
+
doc.title('Counter');
|
|
326
|
+
doc.state('count', 0);
|
|
327
|
+
|
|
328
|
+
// Display
|
|
329
|
+
const display = doc.create('h1');
|
|
330
|
+
display.bind('count', (val) => `Count: ${val}`);
|
|
331
|
+
|
|
332
|
+
// Buttons
|
|
333
|
+
doc.create('button')
|
|
334
|
+
.text('Decrement')
|
|
335
|
+
.on('click', () => { State.count--; });
|
|
336
|
+
|
|
337
|
+
doc.create('button')
|
|
338
|
+
.text('Reset')
|
|
339
|
+
.on('click', () => { State.count = 0; });
|
|
340
|
+
|
|
341
|
+
doc.create('button')
|
|
342
|
+
.text('Increment')
|
|
343
|
+
.on('click', () => { State.count++; });
|
|
344
|
+
|
|
345
|
+
res.send(doc.render());
|
|
346
|
+
});
|
|
347
|
+
```
|
|
181
348
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
349
|
+
### With JSON Export (SPA Mode)
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
app.get('/spa', (req, res) => {
|
|
353
|
+
const doc = new Document();
|
|
354
|
+
doc.state('page', 'home');
|
|
355
|
+
|
|
356
|
+
// Build UI...
|
|
357
|
+
|
|
358
|
+
// Render with obfuscated JSON
|
|
359
|
+
const html = doc.renderJSON({ obfuscate: true });
|
|
360
|
+
res.send(html);
|
|
361
|
+
|
|
362
|
+
// Client can access: window.__SCULPTOR_DATA__
|
|
363
|
+
});
|
|
364
|
+
```
|
|
190
365
|
|
|
191
366
|
---
|
|
192
367
|
|
|
193
|
-
|
|
368
|
+
## Performance
|
|
369
|
+
|
|
370
|
+
### Benchmarks
|
|
371
|
+
|
|
372
|
+
| Scenario | Avg Time | Requests/Sec |
|
|
373
|
+
|----------|----------|--------------|
|
|
374
|
+
| Simple page (10 elements) | 0.5-1ms | 1,000-2,000 |
|
|
375
|
+
| Complex page (100 elements) | 3-5ms | 200-333 |
|
|
376
|
+
| With state (10 bindings) | 2-3ms | 333-500 |
|
|
377
|
+
| Cached page | <0.1ms | 10,000+ |
|
|
378
|
+
|
|
379
|
+
### Memory Usage
|
|
194
380
|
|
|
195
|
-
|
|
381
|
+
- **Per Request:** 50-200 KB
|
|
382
|
+
- **1000 Requests:** ~20-40 MB total
|
|
383
|
+
- **Object Pooling:** Keeps memory stable
|
|
196
384
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
385
|
+
### File Sizes
|
|
386
|
+
|
|
387
|
+
| Output | Size |
|
|
388
|
+
|--------|------|
|
|
389
|
+
| `render()` | 1-5 KB |
|
|
390
|
+
| `renderJSON()` | 2-8 KB |
|
|
391
|
+
| `renderJSON({ obfuscate: true })` | 1-4 KB (50% smaller!) |
|
|
392
|
+
|
|
393
|
+
**Comparison with other solutions:**
|
|
394
|
+
- 2-4x faster than EJS/Pug/Handlebars
|
|
395
|
+
- 10-50x faster than React SSR
|
|
396
|
+
- 50-200x less memory than Next.js
|
|
200
397
|
|
|
201
398
|
---
|
|
202
399
|
|
|
203
|
-
|
|
400
|
+
## Configuration
|
|
204
401
|
|
|
205
|
-
|
|
206
|
-
|
|
402
|
+
```javascript
|
|
403
|
+
const { CONFIG } = require('buildhtml');
|
|
404
|
+
|
|
405
|
+
CONFIG.mode = 'prod'; // 'prod' or 'dev'
|
|
406
|
+
CONFIG.poolSize = 150; // Max pooled elements
|
|
407
|
+
CONFIG.cacheLimit = 2000; // Max cached responses
|
|
408
|
+
CONFIG.enableMetrics = true; // Track performance
|
|
409
|
+
CONFIG.sanitizeCss = true; // CSS injection protection
|
|
410
|
+
```
|
|
207
411
|
|
|
208
412
|
---
|
|
209
413
|
|
|
414
|
+
## Middleware Helpers
|
|
415
|
+
|
|
416
|
+
### createCachedRenderer(builderFn, cacheKeyOrFn, options)
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
app.get('/page', createCachedRenderer(
|
|
420
|
+
async (req) => {
|
|
421
|
+
const doc = new Document();
|
|
422
|
+
// Build page...
|
|
423
|
+
return doc;
|
|
424
|
+
},
|
|
425
|
+
'page-key' // or (req) => `page-${req.params.id}`
|
|
426
|
+
));
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### clearCache(pattern?)
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
clearCache(); // Clear all
|
|
433
|
+
clearCache('user-'); // Clear all keys containing 'user-'
|
|
434
|
+
```
|
|
435
|
+
|
|
210
436
|
### enableCompression()
|
|
211
437
|
|
|
212
|
-
|
|
438
|
+
```javascript
|
|
439
|
+
const { enableCompression } = require('buildhtml');
|
|
440
|
+
app.use(enableCompression());
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### warmupCache(routes)
|
|
444
|
+
|
|
445
|
+
```javascript
|
|
446
|
+
const { warmupCache } = require('buildhtml');
|
|
447
|
+
|
|
448
|
+
await warmupCache([
|
|
449
|
+
{ key: 'home', builder: () => buildHomePage() },
|
|
450
|
+
{ key: 'about', builder: () => buildAboutPage() }
|
|
451
|
+
]);
|
|
452
|
+
```
|
|
213
453
|
|
|
214
454
|
---
|
|
215
455
|
|
|
216
|
-
|
|
456
|
+
## JSON Export/Import
|
|
457
|
+
|
|
458
|
+
### Export
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
const doc = new Document();
|
|
462
|
+
doc.state('count', 0);
|
|
463
|
+
// ... build document ...
|
|
464
|
+
|
|
465
|
+
// Get JSON
|
|
466
|
+
const json = doc.toJSON();
|
|
467
|
+
fs.writeFileSync('./page.json', JSON.stringify(json));
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Import
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
const json = JSON.parse(fs.readFileSync('./page.json'));
|
|
474
|
+
const doc = Document.fromJSON(json);
|
|
475
|
+
const html = doc.render();
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Embedded JSON
|
|
479
|
+
|
|
480
|
+
```javascript
|
|
481
|
+
// Render with JSON embedded in HTML
|
|
482
|
+
const html = doc.renderJSON({ encrypt: true });
|
|
217
483
|
|
|
218
|
-
|
|
484
|
+
// In browser:
|
|
485
|
+
console.log(window.__SCULPTOR_DATA__);
|
|
486
|
+
// Can rebuild page from JSON if needed
|
|
487
|
+
```
|
|
219
488
|
|
|
220
489
|
---
|
|
221
490
|
|
|
222
|
-
|
|
491
|
+
## Security
|
|
492
|
+
|
|
493
|
+
### XSS Protection
|
|
494
|
+
|
|
495
|
+
All text is automatically escaped:
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
doc.create('div').text('<script>alert("XSS")</script>');
|
|
499
|
+
// Output: <script>alert("XSS")</script>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### CSS Injection Protection
|
|
223
503
|
|
|
224
|
-
|
|
504
|
+
Dangerous CSS characters are sanitized:
|
|
225
505
|
|
|
226
|
-
|
|
227
|
-
|
|
506
|
+
```javascript
|
|
507
|
+
el.css({ background: 'red; } body { display: none; }' });
|
|
508
|
+
// Sanitized automatically
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### CSP Nonce Support
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
const doc = new Document({ nonce: 'abc123' });
|
|
515
|
+
// All inline scripts/styles get nonce attribute
|
|
516
|
+
```
|
|
228
517
|
|
|
229
518
|
---
|
|
230
519
|
|
|
231
|
-
|
|
520
|
+
## Advanced Features
|
|
521
|
+
|
|
522
|
+
### Fragments
|
|
523
|
+
|
|
524
|
+
```javascript
|
|
525
|
+
function Header(doc) {
|
|
526
|
+
const header = doc.create('header');
|
|
527
|
+
header.create('h1').text('My Site');
|
|
528
|
+
header.create('nav').text('Navigation');
|
|
529
|
+
return header;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
doc.useFragment(Header);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### OnCreate Hook
|
|
536
|
+
|
|
537
|
+
```javascript
|
|
538
|
+
doc.oncreate(() => {
|
|
539
|
+
console.log('Page loaded!');
|
|
540
|
+
// Initialize analytics, etc.
|
|
541
|
+
});
|
|
542
|
+
```
|
|
232
543
|
|
|
233
|
-
|
|
544
|
+
### Metrics
|
|
545
|
+
|
|
546
|
+
```javascript
|
|
547
|
+
process.env.ENABLE_METRICS = 'true';
|
|
548
|
+
|
|
549
|
+
const { metrics } = require('buildhtml');
|
|
550
|
+
|
|
551
|
+
// After some requests...
|
|
552
|
+
console.log(metrics.getStats());
|
|
553
|
+
// {
|
|
554
|
+
// counters: { 'render.count': 1000 },
|
|
555
|
+
// timings: { 'render.total': { avg: 2.5, p95: 5 } }
|
|
556
|
+
// }
|
|
557
|
+
```
|
|
234
558
|
|
|
235
559
|
---
|
|
236
560
|
|
|
237
|
-
##
|
|
561
|
+
## Best Practices
|
|
238
562
|
|
|
239
|
-
|
|
563
|
+
### ✅ DO
|
|
240
564
|
|
|
241
565
|
```javascript
|
|
242
|
-
|
|
243
|
-
|
|
566
|
+
// Use global state for reactive data
|
|
567
|
+
doc.state('count', 0);
|
|
568
|
+
btn.on('click', () => { State.count++; });
|
|
244
569
|
|
|
245
|
-
|
|
570
|
+
// Cache static pages
|
|
571
|
+
app.get('/about', createCachedRenderer(..., 'about'));
|
|
246
572
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
// -----------------------------------------------------------------------------
|
|
250
|
-
app.get('/', (req, res) => {
|
|
251
|
-
const doc = new Document();
|
|
252
|
-
doc.title('Home Page');
|
|
573
|
+
// Use CSS-in-JS for scoped styles
|
|
574
|
+
el.css({ padding: '20px', backgroundColor: '#f0f0f0' });
|
|
253
575
|
|
|
254
|
-
|
|
255
|
-
|
|
576
|
+
// Leverage object pooling (automatic)
|
|
577
|
+
// Elements are recycled after render()
|
|
578
|
+
```
|
|
256
579
|
|
|
257
|
-
|
|
258
|
-
res.send(doc.render());
|
|
259
|
-
});
|
|
580
|
+
### ❌ DON'T
|
|
260
581
|
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
const doc = new Document({ cache: true, cacheKey: 'about' });
|
|
266
|
-
doc.title('About Us');
|
|
267
|
-
|
|
268
|
-
const content = doc.create('p').text('This page is cached for performance.');
|
|
269
|
-
doc.use(content);
|
|
270
|
-
return doc;
|
|
271
|
-
}, 'about'));
|
|
272
|
-
|
|
273
|
-
// -----------------------------------------------------------------------------
|
|
274
|
-
// 3️⃣ Dynamic Route with Params (Cached per User)
|
|
275
|
-
// -----------------------------------------------------------------------------
|
|
276
|
-
app.get('/user/:name', createCachedRenderer((req) => {
|
|
277
|
-
const doc = new Document({ cache: true, cacheKey: `user-${req.params.name}` });
|
|
278
|
-
doc.title(`Profile - ${req.params.name}`);
|
|
582
|
+
```javascript
|
|
583
|
+
// Don't use closures in event handlers
|
|
584
|
+
let count = 0; // This won't work after serialization
|
|
585
|
+
btn.on('click', () => { count++; });
|
|
279
586
|
|
|
280
|
-
|
|
281
|
-
|
|
587
|
+
// Don't store non-serializable data in state
|
|
588
|
+
doc.state('callback', () => {}); // Functions can't be serialized
|
|
282
589
|
|
|
283
|
-
|
|
284
|
-
|
|
590
|
+
// Don't manually manipulate the DOM
|
|
591
|
+
// Use State instead for reactivity
|
|
592
|
+
```
|
|
285
593
|
|
|
286
|
-
|
|
287
|
-
// 4️⃣ Interactive Counter (No Cache, With State)
|
|
288
|
-
// -----------------------------------------------------------------------------
|
|
289
|
-
app.get('/counter', (req, res) => {
|
|
290
|
-
const doc = new Document();
|
|
291
|
-
doc.title('Counter App');
|
|
594
|
+
---
|
|
292
595
|
|
|
293
|
-
|
|
294
|
-
const incBtn = doc.create('button').text('+');
|
|
295
|
-
incBtn.bindState(counter, 'click', function() {
|
|
296
|
-
const id = '__STATE_ID__';
|
|
297
|
-
window.state[id] = parseInt(window.state[id]) + 1;
|
|
298
|
-
document.getElementById(id).textContent = window.state[id];
|
|
299
|
-
});
|
|
300
|
-
doc.use(counter).use(incBtn);
|
|
596
|
+
## Limitations
|
|
301
597
|
|
|
302
|
-
|
|
303
|
-
|
|
598
|
+
### Function Serialization
|
|
599
|
+
|
|
600
|
+
Event handlers are serialized with `.toString()`:
|
|
601
|
+
|
|
602
|
+
```javascript
|
|
603
|
+
// ❌ BAD - Uses closure (won't work)
|
|
604
|
+
let count = 0;
|
|
605
|
+
btn.on('click', () => { count++; });
|
|
304
606
|
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
607
|
+
// ✅ GOOD - Uses global State
|
|
608
|
+
doc.state('count', 0);
|
|
609
|
+
btn.on('click', () => { State.count++; });
|
|
610
|
+
```
|
|
308
611
|
|
|
309
|
-
###
|
|
612
|
+
### State Values
|
|
310
613
|
|
|
311
|
-
|
|
312
|
-
* **`createCachedRenderer`** – Cache static or dynamic pages for ultra-fast responses.
|
|
313
|
-
* **Stateful elements** – `.state()` allows dynamic, interactive content.
|
|
314
|
-
* **Express-friendly** – Integrates with your server routes seamlessly.
|
|
315
|
-
* **`clearCache()`** – Manually clear cached pages when content changes.
|
|
614
|
+
State must be JSON-serializable:
|
|
316
615
|
|
|
317
|
-
|
|
616
|
+
```javascript
|
|
617
|
+
// ✅ GOOD
|
|
618
|
+
doc.state('user', { name: 'Alice', age: 30 });
|
|
619
|
+
doc.state('items', [1, 2, 3]);
|
|
318
620
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
621
|
+
// ❌ BAD
|
|
622
|
+
doc.state('callback', () => {}); // Functions
|
|
623
|
+
doc.state('dom', document.getElementById('x')); // DOM nodes
|
|
624
|
+
```
|
|
322
625
|
|
|
323
626
|
---
|
|
324
627
|
|
|
325
|
-
##
|
|
628
|
+
## Migration from Other Libraries
|
|
326
629
|
|
|
327
|
-
|
|
630
|
+
### From EJS/Pug/Handlebars
|
|
631
|
+
|
|
632
|
+
```javascript
|
|
633
|
+
// EJS/Pug
|
|
634
|
+
res.render('template', { data })
|
|
635
|
+
|
|
636
|
+
// BuildHTML
|
|
637
|
+
const doc = new Document();
|
|
638
|
+
doc.create('div').text(data);
|
|
639
|
+
res.send(doc.render());
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### From React SSR
|
|
643
|
+
|
|
644
|
+
```javascript
|
|
645
|
+
// React SSR
|
|
646
|
+
const html = renderToString(<App />);
|
|
647
|
+
|
|
648
|
+
// BuildHTML
|
|
649
|
+
const doc = new Document();
|
|
650
|
+
// ... build UI ...
|
|
651
|
+
const html = doc.render();
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Benefits:**
|
|
655
|
+
- 2-4x faster
|
|
656
|
+
- 50-200x less memory
|
|
657
|
+
- No build step required
|
|
658
|
+
- Built-in state management
|
|
328
659
|
|
|
329
660
|
---
|
|
661
|
+
|
|
662
|
+
## Complete Example
|
|
663
|
+
|
|
664
|
+
```javascript
|
|
665
|
+
const express = require('express');
|
|
666
|
+
const { Document, createCachedRenderer } = require('buildhtml');
|
|
667
|
+
|
|
668
|
+
const app = express();
|
|
669
|
+
|
|
670
|
+
// Simple counter with reactive state
|
|
671
|
+
app.get('/counter', (req, res) => {
|
|
672
|
+
const doc = new Document();
|
|
673
|
+
doc.title('Counter App');
|
|
674
|
+
|
|
675
|
+
// Global state
|
|
676
|
+
doc.state('count', 0);
|
|
677
|
+
|
|
678
|
+
// Styled container
|
|
679
|
+
const container = doc.create('div');
|
|
680
|
+
container.css({
|
|
681
|
+
maxWidth: '400px',
|
|
682
|
+
margin: '50px auto',
|
|
683
|
+
padding: '20px',
|
|
684
|
+
textAlign: 'center',
|
|
685
|
+
fontFamily: 'Arial, sans-serif'
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Title
|
|
689
|
+
container.create('h1').text('Counter Demo');
|
|
690
|
+
|
|
691
|
+
// Count display (bound to state)
|
|
692
|
+
const display = container.create('div');
|
|
693
|
+
display.css({
|
|
694
|
+
fontSize: '48px',
|
|
695
|
+
margin: '20px',
|
|
696
|
+
color: '#333'
|
|
697
|
+
});
|
|
698
|
+
display.bind('count', (val) => `Count: ${val}`);
|
|
699
|
+
|
|
700
|
+
// Button container
|
|
701
|
+
const buttons = container.create('div');
|
|
702
|
+
|
|
703
|
+
// Decrement button
|
|
704
|
+
const decBtn = buttons.create('button');
|
|
705
|
+
decBtn.text('− Decrement');
|
|
706
|
+
decBtn.css({
|
|
707
|
+
padding: '10px 20px',
|
|
708
|
+
margin: '5px',
|
|
709
|
+
cursor: 'pointer',
|
|
710
|
+
fontSize: '16px'
|
|
711
|
+
});
|
|
712
|
+
decBtn.on('click', () => { State.count--; });
|
|
713
|
+
|
|
714
|
+
// Reset button
|
|
715
|
+
const resetBtn = buttons.create('button');
|
|
716
|
+
resetBtn.text('Reset');
|
|
717
|
+
resetBtn.css({
|
|
718
|
+
padding: '10px 20px',
|
|
719
|
+
margin: '5px',
|
|
720
|
+
cursor: 'pointer',
|
|
721
|
+
fontSize: '16px'
|
|
722
|
+
});
|
|
723
|
+
resetBtn.on('click', () => { State.count = 0; });
|
|
724
|
+
|
|
725
|
+
// Increment button
|
|
726
|
+
const incBtn = buttons.create('button');
|
|
727
|
+
incBtn.text('+ Increment');
|
|
728
|
+
incBtn.css({
|
|
729
|
+
padding: '10px 20px',
|
|
730
|
+
margin: '5px',
|
|
731
|
+
cursor: 'pointer',
|
|
732
|
+
fontSize: '16px'
|
|
733
|
+
});
|
|
734
|
+
incBtn.on('click', () => { State.count++; });
|
|
735
|
+
|
|
736
|
+
res.send(doc.render());
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// Form with input binding
|
|
740
|
+
app.get('/form', (req, res) => {
|
|
741
|
+
const doc = new Document();
|
|
742
|
+
doc.title('Form Example');
|
|
743
|
+
|
|
744
|
+
// State
|
|
745
|
+
doc.state('username', '');
|
|
746
|
+
doc.state('greeting', 'Enter your name');
|
|
747
|
+
|
|
748
|
+
// Form
|
|
749
|
+
const form = doc.create('div');
|
|
750
|
+
form.css({ padding: '20px', fontFamily: 'Arial' });
|
|
751
|
+
|
|
752
|
+
form.create('h1').text('Form Demo');
|
|
753
|
+
|
|
754
|
+
// Input
|
|
755
|
+
const input = form.create('input');
|
|
756
|
+
input.attr('type', 'text');
|
|
757
|
+
input.attr('placeholder', 'Enter name...');
|
|
758
|
+
input.css({ padding: '10px', fontSize: '16px' });
|
|
759
|
+
|
|
760
|
+
// Submit button
|
|
761
|
+
const submitBtn = form.create('button');
|
|
762
|
+
submitBtn.text('Submit');
|
|
763
|
+
submitBtn.css({ padding: '10px 20px', marginLeft: '10px' });
|
|
764
|
+
submitBtn.on('click', () => {
|
|
765
|
+
const input = document.querySelector('input');
|
|
766
|
+
State.username = input.value;
|
|
767
|
+
State.greeting = `Hello, ${State.username}!`;
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Display greeting
|
|
771
|
+
const greetingEl = form.create('div');
|
|
772
|
+
greetingEl.css({ marginTop: '20px', fontSize: '24px' });
|
|
773
|
+
greetingEl.bind('greeting');
|
|
774
|
+
|
|
775
|
+
res.send(doc.render());
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Cached static page
|
|
779
|
+
app.get('/about', createCachedRenderer(
|
|
780
|
+
async () => {
|
|
781
|
+
const doc = new Document();
|
|
782
|
+
doc.title('About Us');
|
|
783
|
+
|
|
784
|
+
const page = doc.create('div');
|
|
785
|
+
page.css({
|
|
786
|
+
maxWidth: '800px',
|
|
787
|
+
margin: '0 auto',
|
|
788
|
+
padding: '20px',
|
|
789
|
+
fontFamily: 'Arial'
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
page.create('h1').text('About BuildHTML');
|
|
793
|
+
page.create('p').text('High-performance SSR library for Node.js');
|
|
794
|
+
page.create('p').text('Features: Object pooling, reactive state, CSS-in-JS');
|
|
795
|
+
page.create('p').text('This page is cached for maximum performance!');
|
|
796
|
+
|
|
797
|
+
return doc;
|
|
798
|
+
},
|
|
799
|
+
'about-page'
|
|
800
|
+
));
|
|
801
|
+
|
|
802
|
+
app.listen(3000, () => {
|
|
803
|
+
console.log('Server running at http://localhost:3000');
|
|
804
|
+
console.log('Routes:');
|
|
805
|
+
console.log(' /counter - Interactive counter');
|
|
806
|
+
console.log(' /form - Form with state binding');
|
|
807
|
+
console.log(' /about - Cached static page');
|
|
808
|
+
});
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### What This Example Shows
|
|
812
|
+
|
|
813
|
+
✅ **Reactive state binding** - `bind()` automatically updates text
|
|
814
|
+
✅ **Event handling** - Buttons update state
|
|
815
|
+
✅ **CSS-in-JS** - Inline styling with scoped classes
|
|
816
|
+
✅ **Form inputs** - Reading input values in events
|
|
817
|
+
✅ **Cached pages** - Static pages served from cache
|
|
818
|
+
✅ **Auto-attachment** - All elements automatically added
|
|
819
|
+
|
|
820
|
+
### Try It
|
|
821
|
+
|
|
822
|
+
```bash
|
|
823
|
+
node app.js
|
|
824
|
+
# Visit http://localhost:3000/counter
|
|
825
|
+
```
|