boxwood 1.1.0 → 2.0.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 +10 -0
- package/README.md +143 -101
- package/examples/typescript-example.ts +49 -0
- package/index.d.ts +545 -0
- package/index.js +221 -64
- package/package.json +2 -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,117 @@ The template starts with a standard js file, which builds a tree of nodes, that
|
|
|
37
84
|
|
|
38
85
|
## Usage
|
|
39
86
|
|
|
40
|
-
|
|
41
|
-
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" })
|
|
48
|
-
console.log(html)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
You can use [express-boxwood](https://www.npmjs.com/package/express-boxwood) for [express](https://www.npmjs.com/package/express).
|
|
52
|
-
|
|
53
|
-
## Syntax
|
|
87
|
+
Create a template file:
|
|
54
88
|
|
|
55
89
|
```js
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
const banner = require("./banner")
|
|
90
|
+
// templates/greeting.js
|
|
91
|
+
const { Div, H1, P } = require("boxwood")
|
|
59
92
|
|
|
60
|
-
module.exports = () => {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
description: "Lorem ipsum dolor sit amet",
|
|
65
|
-
}),
|
|
93
|
+
module.exports = ({ name, message }) => {
|
|
94
|
+
return Div([
|
|
95
|
+
H1(`Hello, ${name}!`),
|
|
96
|
+
P(message)
|
|
66
97
|
])
|
|
67
98
|
}
|
|
68
99
|
```
|
|
69
100
|
|
|
70
|
-
|
|
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
|
-
)
|
|
89
|
-
```
|
|
101
|
+
Compile and render it:
|
|
90
102
|
|
|
91
103
|
```js
|
|
92
|
-
//
|
|
93
|
-
const {
|
|
104
|
+
// app.js
|
|
105
|
+
const { compile } = require("boxwood")
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
const { template } = compile("./templates/greeting.js")
|
|
108
|
+
const html = template({
|
|
109
|
+
name: "World",
|
|
110
|
+
message: "Welcome to Boxwood"
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
console.log(html)
|
|
114
|
+
// <div><h1>Hello, World!</h1><p>Welcome to Boxwood</p></div>
|
|
98
115
|
```
|
|
99
116
|
|
|
117
|
+
For Express apps, use [express-boxwood](https://www.npmjs.com/package/express-boxwood).
|
|
118
|
+
|
|
119
|
+
## Features
|
|
120
|
+
|
|
121
|
+
### Components with CSS
|
|
122
|
+
|
|
100
123
|
```js
|
|
101
|
-
//
|
|
102
|
-
const { component, css,
|
|
103
|
-
|
|
104
|
-
const styles = css
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
// button.js
|
|
125
|
+
const { component, css, Button: ButtonTag } = require("boxwood")
|
|
126
|
+
|
|
127
|
+
const styles = css`
|
|
128
|
+
.button {
|
|
129
|
+
padding: 8px 16px;
|
|
130
|
+
background: blue;
|
|
131
|
+
color: white;
|
|
132
|
+
}
|
|
133
|
+
.secondary {
|
|
134
|
+
background: gray;
|
|
135
|
+
}
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
const Button = ({ variant, children }) => {
|
|
139
|
+
return ButtonTag({
|
|
140
|
+
// className accepts arrays - falsy values are automatically filtered
|
|
141
|
+
className: [styles.button, variant === 'secondary' && styles.secondary]
|
|
142
|
+
}, children)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = component(Button, { styles })
|
|
115
146
|
```
|
|
116
147
|
|
|
117
|
-
|
|
118
|
-
// example/banner/index.test.js
|
|
119
|
-
const test = require("node:test")
|
|
120
|
-
const assert = require("node:assert")
|
|
121
|
-
const { compile } = require("boxwood")
|
|
148
|
+
### Internationalization
|
|
122
149
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
150
|
+
```js
|
|
151
|
+
// welcome.js
|
|
152
|
+
const { component, i18n, H1, P } = require("boxwood")
|
|
153
|
+
|
|
154
|
+
const Welcome = ({ translate, username }) => {
|
|
155
|
+
return [
|
|
156
|
+
H1(translate("greeting").replace("{name}", username)),
|
|
157
|
+
P(translate("intro"))
|
|
158
|
+
]
|
|
159
|
+
}
|
|
128
160
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const html = template({ title: "foo", description: "bar" })
|
|
132
|
-
assert(html.includes("<h1>foo</h1>"))
|
|
133
|
-
assert(html.includes("<p>bar</p>"))
|
|
161
|
+
module.exports = component(Welcome, {
|
|
162
|
+
i18n: i18n.load(__dirname)
|
|
134
163
|
})
|
|
135
164
|
```
|
|
136
165
|
|
|
137
|
-
|
|
166
|
+
### Asset Loading
|
|
138
167
|
|
|
139
|
-
|
|
168
|
+
```js
|
|
169
|
+
const { Img, Svg } = require("boxwood")
|
|
140
170
|
|
|
141
|
-
|
|
171
|
+
// Load and inline images
|
|
172
|
+
const Logo = Img.load("./assets/logo.png")
|
|
142
173
|
|
|
143
|
-
|
|
174
|
+
// Load and sanitize SVGs
|
|
175
|
+
const Icon = Svg.load("./assets/icon.svg")
|
|
144
176
|
|
|
145
|
-
|
|
177
|
+
module.exports = () => {
|
|
178
|
+
return [Logo(), Icon]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Additional examples are available in the `test` directory.
|
|
183
|
+
|
|
184
|
+
## Security
|
|
146
185
|
|
|
147
|
-
|
|
186
|
+
Boxwood provides basic security features:
|
|
148
187
|
|
|
149
|
-
|
|
188
|
+
- HTML content is escaped by default
|
|
189
|
+
- Loaded SVG and HTML files are sanitized
|
|
190
|
+
- File access is restricted to the project directory
|
|
191
|
+
- Symlinks are blocked to prevent directory traversal
|
|
150
192
|
|
|
151
|
-
|
|
193
|
+
The `sanitize: false` option should only be used with trusted content. Security remains the developer's responsibility.
|
|
152
194
|
|
|
153
195
|
## Contributing
|
|
154
196
|
|
|
155
|
-
|
|
197
|
+
Issues and pull requests are welcome. The codebase is intentionally small and focused.
|
|
156
198
|
|
|
157
199
|
## License
|
|
158
200
|
|
|
@@ -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
|