boxwood 2.2.4 → 2.3.0
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 +39 -29
- package/index.js +3 -13
- package/package.json +1 -1
- package/utilities/hash.js +16 -0
package/README.md
CHANGED
|
@@ -14,10 +14,7 @@ Unlike traditional template engines, Boxwood templates are **just JavaScript fun
|
|
|
14
14
|
const HomePage = ({ posts }) => {
|
|
15
15
|
return Div([
|
|
16
16
|
H1("Blog"),
|
|
17
|
-
posts.map(post => Article([
|
|
18
|
-
H2(post.title),
|
|
19
|
-
P(post.summary)
|
|
20
|
-
]))
|
|
17
|
+
posts.map((post) => Article([H2(post.title), P(post.summary)])),
|
|
21
18
|
])
|
|
22
19
|
}
|
|
23
20
|
```
|
|
@@ -25,48 +22,59 @@ const HomePage = ({ posts }) => {
|
|
|
25
22
|
## Key Advantages
|
|
26
23
|
|
|
27
24
|
### Zero Learning Curve
|
|
25
|
+
|
|
28
26
|
If you know JavaScript, you already know Boxwood. Use `map`, `filter`, `if/else`, and all standard JS features naturally.
|
|
29
27
|
|
|
30
28
|
### IDE Support
|
|
29
|
+
|
|
31
30
|
Get autocomplete, refactoring, and go-to-definition out of the box. Your templates are just code, so your editor understands them.
|
|
32
31
|
|
|
33
32
|
### True Composition
|
|
33
|
+
|
|
34
34
|
Components are functions. Compose them like functions. No slots, no special APIs - just parameters and return values.
|
|
35
35
|
|
|
36
36
|
### Performance
|
|
37
|
+
|
|
37
38
|
No template parsing at runtime. Templates are already JavaScript functions, eliminating parsing overhead.
|
|
38
39
|
|
|
39
40
|
### Security Helpers
|
|
41
|
+
|
|
40
42
|
- Automatic HTML escaping by default
|
|
41
43
|
- Basic sanitization for loaded SVG/HTML files
|
|
42
44
|
- Path traversal protection for file operations
|
|
43
45
|
- Remember: security is ultimately your responsibility
|
|
44
46
|
|
|
45
47
|
### Integrated CSS Management
|
|
48
|
+
|
|
46
49
|
- Automatic CSS scoping with hash-based class names
|
|
47
50
|
- CSS-in-JS with zero runtime
|
|
48
51
|
- Critical CSS inlining
|
|
49
52
|
- Automatic minification
|
|
50
53
|
|
|
51
54
|
### Built-in i18n Support
|
|
55
|
+
|
|
52
56
|
First-class internationalization support with a simple, component-friendly API for multi-language applications.
|
|
53
57
|
|
|
54
58
|
### Asset Handling
|
|
59
|
+
|
|
55
60
|
- Inline images as base64
|
|
56
61
|
- SVG loading with automatic sanitization
|
|
57
62
|
- JSON data loading
|
|
58
63
|
- Raw HTML imports with XSS protection
|
|
59
64
|
|
|
60
65
|
### SEO Friendly
|
|
66
|
+
|
|
61
67
|
- Pure server-side rendering - search engines see fully rendered HTML
|
|
62
68
|
- Lightning fast pages with inlined critical CSS
|
|
63
69
|
- Minimal payload size improves Core Web Vitals scores
|
|
64
70
|
- No client-side hydration delays
|
|
65
71
|
|
|
66
72
|
### Minimal Footprint
|
|
67
|
-
|
|
73
|
+
|
|
74
|
+
Short implementation. No complex build process or heavy dependencies.
|
|
68
75
|
|
|
69
76
|
### Testable by Design
|
|
77
|
+
|
|
70
78
|
Templates are pure functions - easy to unit test with any testing framework.
|
|
71
79
|
|
|
72
80
|
## Table of Contents
|
|
@@ -91,10 +99,7 @@ Create a template file:
|
|
|
91
99
|
const { Div, H1, P } = require("boxwood")
|
|
92
100
|
|
|
93
101
|
module.exports = ({ name, message }) => {
|
|
94
|
-
return Div([
|
|
95
|
-
H1(`Hello, ${name}!`),
|
|
96
|
-
P(message)
|
|
97
|
-
])
|
|
102
|
+
return Div([H1(`Hello, ${name}!`), P(message)])
|
|
98
103
|
}
|
|
99
104
|
```
|
|
100
105
|
|
|
@@ -105,9 +110,9 @@ Compile and render it:
|
|
|
105
110
|
const { compile } = require("boxwood")
|
|
106
111
|
|
|
107
112
|
const { template } = compile("./templates/greeting.js")
|
|
108
|
-
const html = template({
|
|
113
|
+
const html = template({
|
|
109
114
|
name: "World",
|
|
110
|
-
message: "Welcome to Boxwood"
|
|
115
|
+
message: "Welcome to Boxwood",
|
|
111
116
|
})
|
|
112
117
|
|
|
113
118
|
console.log(html)
|
|
@@ -119,40 +124,41 @@ console.log(html)
|
|
|
119
124
|
Boxwood includes built-in Express support:
|
|
120
125
|
|
|
121
126
|
```js
|
|
122
|
-
import express from
|
|
123
|
-
import engine from
|
|
124
|
-
import crypto from
|
|
127
|
+
import express from "express"
|
|
128
|
+
import engine from "boxwood/adapters/express"
|
|
129
|
+
import crypto from "crypto"
|
|
125
130
|
|
|
126
131
|
const app = express()
|
|
127
132
|
|
|
128
133
|
// Register Boxwood as template engine
|
|
129
|
-
app.engine(
|
|
130
|
-
app.set(
|
|
131
|
-
app.set(
|
|
134
|
+
app.engine("js", engine())
|
|
135
|
+
app.set("views", "./views")
|
|
136
|
+
app.set("view engine", "js")
|
|
132
137
|
|
|
133
138
|
// CSP (Content Security Policy) nonce for inline scripts
|
|
134
139
|
// A nonce is a unique random value generated for each request that allows
|
|
135
140
|
// specific inline scripts to execute while blocking potential XSS attacks
|
|
136
141
|
app.use((req, res, next) => {
|
|
137
142
|
// Generate a cryptographically secure random nonce
|
|
138
|
-
res.locals.nonce = crypto.randomBytes(16).toString(
|
|
139
|
-
|
|
143
|
+
res.locals.nonce = crypto.randomBytes(16).toString("base64")
|
|
144
|
+
|
|
140
145
|
// Set CSP header - only scripts with this exact nonce can execute
|
|
141
146
|
res.setHeader(
|
|
142
|
-
|
|
147
|
+
"Content-Security-Policy",
|
|
143
148
|
`script-src 'nonce-${res.locals.nonce}' 'strict-dynamic';`
|
|
144
149
|
)
|
|
145
150
|
next()
|
|
146
151
|
})
|
|
147
152
|
|
|
148
153
|
// Render templates - nonce is automatically injected into all inline scripts
|
|
149
|
-
app.get(
|
|
150
|
-
res.render(
|
|
154
|
+
app.get("/", (req, res) => {
|
|
155
|
+
res.render("home", { title: "Welcome" })
|
|
151
156
|
// Boxwood automatically adds nonce="${res.locals.nonce}" to script tags
|
|
152
157
|
})
|
|
153
158
|
```
|
|
154
159
|
|
|
155
160
|
The Express adapter automatically:
|
|
161
|
+
|
|
156
162
|
- Handles template caching in production
|
|
157
163
|
- Hot reloads templates in development
|
|
158
164
|
- Injects CSP nonces from `res.locals.nonce` into all inline scripts and styles
|
|
@@ -171,6 +177,7 @@ A Content Security Policy (CSP) nonce is a security feature that helps prevent C
|
|
|
171
177
|
- Attackers can't guess the nonce, so injected scripts are blocked
|
|
172
178
|
|
|
173
179
|
Example output:
|
|
180
|
+
|
|
174
181
|
```html
|
|
175
182
|
<!-- HTTP Header -->
|
|
176
183
|
Content-Security-Policy: script-src 'nonce-rAnd0m123' 'strict-dynamic';
|
|
@@ -205,10 +212,13 @@ const styles = css`
|
|
|
205
212
|
`
|
|
206
213
|
|
|
207
214
|
const Button = ({ variant, children }) => {
|
|
208
|
-
return ButtonTag(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
return ButtonTag(
|
|
216
|
+
{
|
|
217
|
+
// className accepts arrays - falsy values are automatically filtered
|
|
218
|
+
className: [styles.button, variant === "secondary" && styles.secondary],
|
|
219
|
+
},
|
|
220
|
+
children
|
|
221
|
+
)
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
module.exports = component(Button, { styles })
|
|
@@ -223,12 +233,12 @@ const { component, i18n, H1, P } = require("boxwood")
|
|
|
223
233
|
const Welcome = ({ translate, username }) => {
|
|
224
234
|
return [
|
|
225
235
|
H1(translate("greeting").replace("{name}", username)),
|
|
226
|
-
P(translate("intro"))
|
|
236
|
+
P(translate("intro")),
|
|
227
237
|
]
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
module.exports = component(Welcome, {
|
|
231
|
-
i18n: i18n.load(__dirname)
|
|
240
|
+
module.exports = component(Welcome, {
|
|
241
|
+
i18n: i18n.load(__dirname),
|
|
232
242
|
})
|
|
233
243
|
```
|
|
234
244
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { join, resolve, sep: separator } = require("path")
|
|
2
2
|
const { readFileSync, realpathSync, lstatSync } = require("fs")
|
|
3
3
|
const csstree = require("css-tree")
|
|
4
|
+
const { createHash } = require("./utilities/hash")
|
|
4
5
|
|
|
5
6
|
function compile(path) {
|
|
6
7
|
const fn = require(path)
|
|
@@ -493,16 +494,6 @@ const tag = (a, b, c) => {
|
|
|
493
494
|
}
|
|
494
495
|
}
|
|
495
496
|
|
|
496
|
-
// DJB2 hash algorithm for CSS class names
|
|
497
|
-
function hashDJB2(str) {
|
|
498
|
-
let hash = 5381
|
|
499
|
-
for (let i = 0; i < str.length; i++) {
|
|
500
|
-
hash = (hash * 33) ^ str.charCodeAt(i)
|
|
501
|
-
}
|
|
502
|
-
// Convert to positive number and base36 for shorter string
|
|
503
|
-
return (hash >>> 0).toString(36)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
497
|
function decamelize(string) {
|
|
507
498
|
return string.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
|
508
499
|
}
|
|
@@ -561,9 +552,8 @@ function css(inputs) {
|
|
|
561
552
|
|
|
562
553
|
csstree.walk(tree, (node) => {
|
|
563
554
|
if (node.type === "ClassSelector") {
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
const name = `${node.name}_${hash}`
|
|
555
|
+
const hash = createHash(result + node.name)
|
|
556
|
+
const name = hash
|
|
567
557
|
classes[node.name] = name
|
|
568
558
|
node.name = name
|
|
569
559
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let index = 0
|
|
2
|
+
const map = new Map()
|
|
3
|
+
|
|
4
|
+
function createHash(string) {
|
|
5
|
+
if (map.has(string)) {
|
|
6
|
+
return map.get(string)
|
|
7
|
+
}
|
|
8
|
+
index++
|
|
9
|
+
const hash = "c" + index
|
|
10
|
+
map.set(string, hash)
|
|
11
|
+
return hash
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
createHash,
|
|
16
|
+
}
|