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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node test:*)",
5
+ "Bash(mkdir:*)",
6
+ "Bash(npm test:*)",
7
+ "Bash(mv:*)"
8
+ ],
9
+ "deny": []
10
+ }
11
+ }
package/README.md CHANGED
@@ -3,24 +3,71 @@
3
3
  [![npm](https://img.shields.io/npm/v/boxwood.svg)](https://www.npmjs.com/package/boxwood)
4
4
  [![build](https://github.com/buxlabs/boxwood/workflows/build/badge.svg)](https://github.com/buxlabs/boxwood/actions)
5
5
 
6
- > Server side templating engine written in JavaScript
6
+ > It's just JavaScript™ - A template engine that gets out of your way
7
7
 
8
- [boxwood](https://github.com/buxlabs/boxwood) was created to achieve the following design goals:
8
+ ## Why Boxwood?
9
9
 
10
- 1. templates can be split into components
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
- The template starts with a standard js file, which builds a tree of nodes, that get rendered to html.
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
- const { join } = require("path")
43
- // ...
44
- const path = join(__dirname, "index.js")
45
- const { template } = compile(path)
46
- // ...
47
- const html = template({ foo: "bar" })
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
- You can use [express-boxwood](https://www.npmjs.com/package/express-boxwood) for [express](https://www.npmjs.com/package/express).
117
+ ### Express Integration
52
118
 
53
- ## Syntax
119
+ Boxwood includes built-in Express support:
54
120
 
55
121
  ```js
56
- // example/index.js
57
- const layout = require("./layout")
58
- const banner = require("./banner")
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
- module.exports = () => {
61
- return layout({ language: "en" }, [
62
- banner({
63
- title: "Hello, world!",
64
- description: "Lorem ipsum dolor sit amet",
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
- ```js
71
- // example/layout/index.js
72
- const { component, css, doctype, html, body } = require("boxwood")
73
- const head = require("./head")
74
-
75
- const styles = css.load(__dirname)
76
-
77
- module.exports = component(
78
- ({ language }, children) => {
79
- return [
80
- doctype(),
81
- html({ lang: language }, [
82
- head(),
83
- body({ className: styles.layout }, children),
84
- ]),
85
- ]
86
- },
87
- { styles }
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
- ```js
92
- // example/layout/head/index.js
93
- const { head, title } = require("boxwood")
188
+ ## Features
94
189
 
95
- module.exports = () => {
96
- return head([title("example")])
97
- }
98
- ```
190
+ ### Components with CSS
99
191
 
100
192
  ```js
101
- // example/banner/index.js
102
- const { component, css, h1, p, section } = require("boxwood")
103
-
104
- const styles = css.load(__dirname)
105
-
106
- module.exports = component(
107
- ({ title, description }) => {
108
- return section({ className: styles.banner }, [
109
- h1(title),
110
- description && p(description),
111
- ])
112
- },
113
- { styles }
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
- ```js
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
- test("banner renders a title", async () => {
124
- const { template } = compile(__dirname)
125
- const html = template({ title: "foo" })
126
- assert(html.includes("<h1>foo</h1>"))
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
- test("banner renders an optional description", async () => {
130
- const { template } = compile(__dirname)
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
- You can check the `test` dir for more examples.
235
+ ### Asset Loading
138
236
 
139
- ## Security
237
+ ```js
238
+ const { Img, Svg } = require("boxwood")
140
239
 
141
- By default, boxwood sanitizes all HTML, SVG and i18n content loaded via its API to protect against basic XSS attacks.
240
+ // Load and inline images
241
+ const Logo = Img.load("./assets/logo.png")
142
242
 
143
- Disabling sanitization ({ sanitize: false }) is only safe for trusted, developer-controlled files. Never use it with user-generated or untrusted content.
243
+ // Load and sanitize SVGs
244
+ const Icon = Svg.load("./assets/icon.svg")
144
245
 
145
- All file access is restricted to the project directory and symlinks are not allowed by default to prevent path traversal attacks.
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
- That said, the library is pretty small so please review it and suggest improvements if you have any.
255
+ Boxwood provides basic security features:
148
256
 
149
- ## Maintainers
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
- [@emilos](https://github.com/emilos)
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
- All contributions are highly appreciated. Please feel free to open new issues and send PRs.
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