boxwood 1.1.1 → 2.1.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/.claude/settings.local.json +11 -0
- package/README.md +209 -98
- package/adapters/express/index.js +49 -0
- package/examples/typescript-example.ts +49 -0
- package/index.d.ts +545 -0
- package/index.js +168 -64
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -3,24 +3,71 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/boxwood)
|
|
4
4
|
[](https://github.com/buxlabs/boxwood/actions)
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> It's just JavaScript™ - A template engine that gets out of your way
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Why Boxwood?
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
2. css is hashed per component
|
|
12
|
-
3. css is automatically minified
|
|
13
|
-
4. critical css is inlined
|
|
14
|
-
5. templates can import other dependencies
|
|
15
|
-
6. inline images or svgs
|
|
16
|
-
7. i18n support
|
|
17
|
-
8. server side
|
|
18
|
-
9. good for seo
|
|
19
|
-
10. small (1 file, 890 LOC~)
|
|
20
|
-
11. easy to start, familiar syntax
|
|
21
|
-
12. easy to test
|
|
10
|
+
Unlike traditional template engines, Boxwood templates are **just JavaScript functions**. No new syntax to learn, no parsing overhead, and full access to the JavaScript ecosystem.
|
|
22
11
|
|
|
23
|
-
|
|
12
|
+
```javascript
|
|
13
|
+
// This is your template - just a function that returns HTML nodes
|
|
14
|
+
const HomePage = ({ posts }) => {
|
|
15
|
+
return Div([
|
|
16
|
+
H1("Blog"),
|
|
17
|
+
posts.map(post => Article([
|
|
18
|
+
H2(post.title),
|
|
19
|
+
P(post.summary)
|
|
20
|
+
]))
|
|
21
|
+
])
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Key Advantages
|
|
26
|
+
|
|
27
|
+
### Zero Learning Curve
|
|
28
|
+
If you know JavaScript, you already know Boxwood. Use `map`, `filter`, `if/else`, and all standard JS features naturally.
|
|
29
|
+
|
|
30
|
+
### IDE Support
|
|
31
|
+
Get autocomplete, refactoring, and go-to-definition out of the box. Your templates are just code, so your editor understands them.
|
|
32
|
+
|
|
33
|
+
### True Composition
|
|
34
|
+
Components are functions. Compose them like functions. No slots, no special APIs - just parameters and return values.
|
|
35
|
+
|
|
36
|
+
### Performance
|
|
37
|
+
No template parsing at runtime. Templates are already JavaScript functions, eliminating parsing overhead.
|
|
38
|
+
|
|
39
|
+
### Security Helpers
|
|
40
|
+
- Automatic HTML escaping by default
|
|
41
|
+
- Basic sanitization for loaded SVG/HTML files
|
|
42
|
+
- Path traversal protection for file operations
|
|
43
|
+
- Remember: security is ultimately your responsibility
|
|
44
|
+
|
|
45
|
+
### Integrated CSS Management
|
|
46
|
+
- Automatic CSS scoping with hash-based class names
|
|
47
|
+
- CSS-in-JS with zero runtime
|
|
48
|
+
- Critical CSS inlining
|
|
49
|
+
- Automatic minification
|
|
50
|
+
|
|
51
|
+
### Built-in i18n Support
|
|
52
|
+
First-class internationalization support with a simple, component-friendly API for multi-language applications.
|
|
53
|
+
|
|
54
|
+
### Asset Handling
|
|
55
|
+
- Inline images as base64
|
|
56
|
+
- SVG loading with automatic sanitization
|
|
57
|
+
- JSON data loading
|
|
58
|
+
- Raw HTML imports with XSS protection
|
|
59
|
+
|
|
60
|
+
### SEO Friendly
|
|
61
|
+
- Pure server-side rendering - search engines see fully rendered HTML
|
|
62
|
+
- Lightning fast pages with inlined critical CSS
|
|
63
|
+
- Minimal payload size improves Core Web Vitals scores
|
|
64
|
+
- No client-side hydration delays
|
|
65
|
+
|
|
66
|
+
### Minimal Footprint
|
|
67
|
+
Single file implementation (~950 lines). No complex build process or heavy dependencies.
|
|
68
|
+
|
|
69
|
+
### Testable by Design
|
|
70
|
+
Templates are pure functions - easy to unit test with any testing framework.
|
|
24
71
|
|
|
25
72
|
## Table of Contents
|
|
26
73
|
|
|
@@ -37,122 +84,186 @@ The template starts with a standard js file, which builds a tree of nodes, that
|
|
|
37
84
|
|
|
38
85
|
## Usage
|
|
39
86
|
|
|
87
|
+
Create a template file:
|
|
88
|
+
|
|
40
89
|
```js
|
|
90
|
+
// templates/greeting.js
|
|
91
|
+
const { Div, H1, P } = require("boxwood")
|
|
92
|
+
|
|
93
|
+
module.exports = ({ name, message }) => {
|
|
94
|
+
return Div([
|
|
95
|
+
H1(`Hello, ${name}!`),
|
|
96
|
+
P(message)
|
|
97
|
+
])
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Compile and render it:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// app.js
|
|
41
105
|
const { compile } = require("boxwood")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
|
|
107
|
+
const { template } = compile("./templates/greeting.js")
|
|
108
|
+
const html = template({
|
|
109
|
+
name: "World",
|
|
110
|
+
message: "Welcome to Boxwood"
|
|
111
|
+
})
|
|
112
|
+
|
|
48
113
|
console.log(html)
|
|
114
|
+
// <div><h1>Hello, World!</h1><p>Welcome to Boxwood</p></div>
|
|
49
115
|
```
|
|
50
116
|
|
|
51
|
-
|
|
117
|
+
### Express Integration
|
|
52
118
|
|
|
53
|
-
|
|
119
|
+
Boxwood includes built-in Express support:
|
|
54
120
|
|
|
55
121
|
```js
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
122
|
+
import express from 'express'
|
|
123
|
+
import engine from 'boxwood/adapters/express'
|
|
124
|
+
import crypto from 'crypto'
|
|
125
|
+
|
|
126
|
+
const app = express()
|
|
127
|
+
|
|
128
|
+
// Register Boxwood as template engine
|
|
129
|
+
app.engine('js', engine())
|
|
130
|
+
app.set('views', './views')
|
|
131
|
+
app.set('view engine', 'js')
|
|
132
|
+
|
|
133
|
+
// CSP (Content Security Policy) nonce for inline scripts
|
|
134
|
+
// A nonce is a unique random value generated for each request that allows
|
|
135
|
+
// specific inline scripts to execute while blocking potential XSS attacks
|
|
136
|
+
app.use((req, res, next) => {
|
|
137
|
+
// Generate a cryptographically secure random nonce
|
|
138
|
+
res.locals.nonce = crypto.randomBytes(16).toString('base64')
|
|
139
|
+
|
|
140
|
+
// Set CSP header - only scripts with this exact nonce can execute
|
|
141
|
+
res.setHeader(
|
|
142
|
+
'Content-Security-Policy',
|
|
143
|
+
`script-src 'nonce-${res.locals.nonce}' 'strict-dynamic';`
|
|
144
|
+
)
|
|
145
|
+
next()
|
|
146
|
+
})
|
|
59
147
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}),
|
|
66
|
-
])
|
|
67
|
-
}
|
|
148
|
+
// Render templates - nonce is automatically injected into all inline scripts
|
|
149
|
+
app.get('/', (req, res) => {
|
|
150
|
+
res.render('home', { title: 'Welcome' })
|
|
151
|
+
// Boxwood automatically adds nonce="${res.locals.nonce}" to script tags
|
|
152
|
+
})
|
|
68
153
|
```
|
|
69
154
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
155
|
+
The Express adapter automatically:
|
|
156
|
+
- Handles template caching in production
|
|
157
|
+
- Hot reloads templates in development
|
|
158
|
+
- Injects CSP nonces from `res.locals.nonce` into all inline scripts and styles
|
|
159
|
+
|
|
160
|
+
#### Understanding CSP Nonces
|
|
161
|
+
|
|
162
|
+
A Content Security Policy (CSP) nonce is a security feature that helps prevent Cross-Site Scripting (XSS) attacks:
|
|
163
|
+
|
|
164
|
+
1. **Without CSP**: Any injected `<script>` tag can execute, making XSS attacks possible
|
|
165
|
+
2. **With CSP nonce**: Only scripts with the correct nonce attribute can run
|
|
166
|
+
3. **How it works**:
|
|
167
|
+
- Server generates a unique random nonce for each request
|
|
168
|
+
- Server adds this nonce to the CSP header: `script-src 'nonce-abc123'`
|
|
169
|
+
- Server adds the same nonce to legitimate scripts: `<script nonce="abc123">`
|
|
170
|
+
- Browser only executes scripts that have the matching nonce
|
|
171
|
+
- Attackers can't guess the nonce, so injected scripts are blocked
|
|
172
|
+
|
|
173
|
+
Example output:
|
|
174
|
+
```html
|
|
175
|
+
<!-- HTTP Header -->
|
|
176
|
+
Content-Security-Policy: script-src 'nonce-rAnd0m123' 'strict-dynamic';
|
|
177
|
+
|
|
178
|
+
<!-- Generated HTML -->
|
|
179
|
+
<script nonce="rAnd0m123">
|
|
180
|
+
console.log("This legitimate script will execute")
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<script>
|
|
184
|
+
console.log("This injected script will be blocked!")
|
|
185
|
+
</script>
|
|
89
186
|
```
|
|
90
187
|
|
|
91
|
-
|
|
92
|
-
// example/layout/head/index.js
|
|
93
|
-
const { head, title } = require("boxwood")
|
|
188
|
+
## Features
|
|
94
189
|
|
|
95
|
-
|
|
96
|
-
return head([title("example")])
|
|
97
|
-
}
|
|
98
|
-
```
|
|
190
|
+
### Components with CSS
|
|
99
191
|
|
|
100
192
|
```js
|
|
101
|
-
//
|
|
102
|
-
const { component, css,
|
|
103
|
-
|
|
104
|
-
const styles = css
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
193
|
+
// button.js
|
|
194
|
+
const { component, css, Button: ButtonTag } = require("boxwood")
|
|
195
|
+
|
|
196
|
+
const styles = css`
|
|
197
|
+
.button {
|
|
198
|
+
padding: 8px 16px;
|
|
199
|
+
background: blue;
|
|
200
|
+
color: white;
|
|
201
|
+
}
|
|
202
|
+
.secondary {
|
|
203
|
+
background: gray;
|
|
204
|
+
}
|
|
205
|
+
`
|
|
206
|
+
|
|
207
|
+
const Button = ({ variant, children }) => {
|
|
208
|
+
return ButtonTag({
|
|
209
|
+
// className accepts arrays - falsy values are automatically filtered
|
|
210
|
+
className: [styles.button, variant === 'secondary' && styles.secondary]
|
|
211
|
+
}, children)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = component(Button, { styles })
|
|
115
215
|
```
|
|
116
216
|
|
|
117
|
-
|
|
118
|
-
// example/banner/index.test.js
|
|
119
|
-
const test = require("node:test")
|
|
120
|
-
const assert = require("node:assert")
|
|
121
|
-
const { compile } = require("boxwood")
|
|
217
|
+
### Internationalization
|
|
122
218
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
219
|
+
```js
|
|
220
|
+
// welcome.js
|
|
221
|
+
const { component, i18n, H1, P } = require("boxwood")
|
|
222
|
+
|
|
223
|
+
const Welcome = ({ translate, username }) => {
|
|
224
|
+
return [
|
|
225
|
+
H1(translate("greeting").replace("{name}", username)),
|
|
226
|
+
P(translate("intro"))
|
|
227
|
+
]
|
|
228
|
+
}
|
|
128
229
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const html = template({ title: "foo", description: "bar" })
|
|
132
|
-
assert(html.includes("<h1>foo</h1>"))
|
|
133
|
-
assert(html.includes("<p>bar</p>"))
|
|
230
|
+
module.exports = component(Welcome, {
|
|
231
|
+
i18n: i18n.load(__dirname)
|
|
134
232
|
})
|
|
135
233
|
```
|
|
136
234
|
|
|
137
|
-
|
|
235
|
+
### Asset Loading
|
|
138
236
|
|
|
139
|
-
|
|
237
|
+
```js
|
|
238
|
+
const { Img, Svg } = require("boxwood")
|
|
140
239
|
|
|
141
|
-
|
|
240
|
+
// Load and inline images
|
|
241
|
+
const Logo = Img.load("./assets/logo.png")
|
|
142
242
|
|
|
143
|
-
|
|
243
|
+
// Load and sanitize SVGs
|
|
244
|
+
const Icon = Svg.load("./assets/icon.svg")
|
|
144
245
|
|
|
145
|
-
|
|
246
|
+
module.exports = () => {
|
|
247
|
+
return [Logo(), Icon]
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Additional examples are available in the `test` directory.
|
|
252
|
+
|
|
253
|
+
## Security
|
|
146
254
|
|
|
147
|
-
|
|
255
|
+
Boxwood provides basic security features:
|
|
148
256
|
|
|
149
|
-
|
|
257
|
+
- HTML content is escaped by default
|
|
258
|
+
- Loaded SVG and HTML files are sanitized
|
|
259
|
+
- File access is restricted to the project directory
|
|
260
|
+
- Symlinks are blocked to prevent directory traversal
|
|
150
261
|
|
|
151
|
-
|
|
262
|
+
The `sanitize: false` option should only be used with trusted content. Security remains the developer's responsibility.
|
|
152
263
|
|
|
153
264
|
## Contributing
|
|
154
265
|
|
|
155
|
-
|
|
266
|
+
Issues and pull requests are welcome. The codebase is intentionally small and focused.
|
|
156
267
|
|
|
157
268
|
## License
|
|
158
269
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const NODE_ENV = process.env.NODE_ENV || "development"
|
|
2
|
+
const { compile } = require("../..")
|
|
3
|
+
|
|
4
|
+
function purge(path) {
|
|
5
|
+
const name = require.resolve(path)
|
|
6
|
+
const dependency = require.cache[name]
|
|
7
|
+
if (dependency && dependency.children) {
|
|
8
|
+
dependency.children.forEach((child) => {
|
|
9
|
+
delete require.cache[child.id]
|
|
10
|
+
purge(child.id)
|
|
11
|
+
})
|
|
12
|
+
delete require.cache[name]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function engine(options = {}) {
|
|
17
|
+
const env = options.env || NODE_ENV
|
|
18
|
+
const cache = new Map()
|
|
19
|
+
async function compileFile(path) {
|
|
20
|
+
if (env === "development") {
|
|
21
|
+
purge(path)
|
|
22
|
+
const { template } = await compile(path)
|
|
23
|
+
return template
|
|
24
|
+
}
|
|
25
|
+
if (cache.has(path)) return cache.get(path)
|
|
26
|
+
const { template } = await compile(path)
|
|
27
|
+
cache.set(path, template)
|
|
28
|
+
return template
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function render(path, options, callback) {
|
|
32
|
+
try {
|
|
33
|
+
const template = await compileFile(path)
|
|
34
|
+
// Automatically inject nonce from res.locals if available
|
|
35
|
+
const templateOptions = options && options._locals && options._locals.nonce
|
|
36
|
+
? { ...options, nonce: options._locals.nonce }
|
|
37
|
+
: options
|
|
38
|
+
const html = template(templateOptions)
|
|
39
|
+
if (callback) return callback(null, html)
|
|
40
|
+
return html
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (callback) return callback(error)
|
|
43
|
+
return error.message
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return render
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = engine
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Example of using boxwood with TypeScript
|
|
2
|
+
import { Div, H1, Button, Form, Input, component, classes } from 'boxwood';
|
|
3
|
+
|
|
4
|
+
// Define typed component props
|
|
5
|
+
interface UserCardProps {
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Create a typed component
|
|
12
|
+
const UserCard = component<UserCardProps>(
|
|
13
|
+
({ name, email, isActive }, children) => {
|
|
14
|
+
return Div({
|
|
15
|
+
className: classes('user-card', { active: isActive })
|
|
16
|
+
}, [
|
|
17
|
+
H1({}, name),
|
|
18
|
+
Div({ className: 'email' }, email),
|
|
19
|
+
children
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Use the component with type checking
|
|
25
|
+
const app = Div({ id: 'app' }, [
|
|
26
|
+
UserCard({
|
|
27
|
+
name: 'John Doe',
|
|
28
|
+
email: 'john@example.com',
|
|
29
|
+
isActive: true
|
|
30
|
+
},
|
|
31
|
+
Button({ onclick: () => alert('Hello!') }, 'Click me')
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
Form({ method: 'post' }, [
|
|
35
|
+
Input({
|
|
36
|
+
type: 'email',
|
|
37
|
+
name: 'email',
|
|
38
|
+
required: true,
|
|
39
|
+
placeholder: 'Enter email'
|
|
40
|
+
}),
|
|
41
|
+
Button({ type: 'submit' }, 'Submit')
|
|
42
|
+
])
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// TypeScript will provide:
|
|
46
|
+
// - Autocomplete for all element attributes
|
|
47
|
+
// - Type checking for attribute values
|
|
48
|
+
// - Error highlighting for invalid props
|
|
49
|
+
// - IntelliSense documentation
|