create-gardener 2.1.4 → 2.1.5
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 +108 -302
- package/package.json +1 -1
- package/template/.env +3 -0
- package/template/README.md +181 -0
- package/template/src/backend/controllers/gardener/hotReload.ts +8 -8
- package/template/src/backend/controllers/gardener/imageOptimiser.ts +114 -25
- package/template/src/backend/controllers/gardener/index.ts +1 -0
- package/template/src/backend/routes/gardener.route.ts +1 -2
- package/template/src/backend/server.ts +19 -1
- package/template/src/frontend/assets/remote/betterway.jpg +0 -0
- package/template/src/frontend/static/style.css +0 -16
- package/template/src/frontend/views/_.ejs +3 -5
- package/template/src/frontend/static/cache/favicon_500x500.webp +0 -0
- package/template/src/frontend/static/cache/favicon_50x50.webp +0 -0
- package/template/src/frontend/static/cache/gardener_100x100.webp +0 -0
- package/template/src/frontend/static/cache/gardener_500x500.webp +0 -0
package/README.md
CHANGED
|
@@ -1,360 +1,165 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Gardener — DOM-First Mini Framework
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Gardener is a lightweight web framework built on top of Express and EJS. It enables building web applications without React or heavy frontend frameworks by using a DOM-first approach with reusable components.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Create a new Gardener app:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pnpm create gardener app
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Install dependencies and start development:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pnpm install
|
|
17
|
-
pnpm dev
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Your app will be running with hot reload enabled!
|
|
21
|
-
|
|
22
|
-
## 🌟 Core Features
|
|
23
|
-
|
|
24
|
-
### Backend
|
|
25
|
-
- **Express.js**: Familiar Node.js backend with full Express capabilities
|
|
26
|
-
- **TypeScript Ready**: Built-in TypeScript support for type-safe development
|
|
27
|
-
- **Static Site Generation**: Export your dynamic app to static HTML with one API call
|
|
28
|
-
|
|
29
|
-
### Frontend
|
|
30
|
-
- **EJS Templating**: Server-side rendering with EJS views
|
|
31
|
-
- **Tailwind CSS**: Integrated with watch mode for rapid styling
|
|
32
|
-
- **Gardener Component System**: Unique DOM-to-JSON conversion for reusable components
|
|
33
|
-
- **Live Development Tools**: Browser-based route and component creation
|
|
34
|
-
|
|
35
|
-
### Developer Experience
|
|
36
|
-
- **Hot Reload**: Toggle with `Alt + H` for instant CSS updates
|
|
37
|
-
- **Visual Component Parser**: Convert DOM elements to JSON components in the browser
|
|
38
|
-
- **Browser-Based Routing**: Create new routes without touching your code
|
|
39
|
-
- **Image Optimization**: Automatic WebP conversion with smart sizing
|
|
40
|
-
|
|
41
|
-
## 📖 Complete Workflow
|
|
42
|
-
|
|
43
|
-
### 1. Backend Development
|
|
44
|
-
|
|
45
|
-
Write familiar Express.js code in TypeScript:
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// src/backend/server.ts
|
|
49
|
-
app.get('/api/posts', (req, res) => {
|
|
50
|
-
res.json({ posts: [] });
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### 2. Creating Pages & Routes
|
|
55
|
-
|
|
56
|
-
**Two ways to create routes:**
|
|
5
|
+
---
|
|
57
6
|
|
|
58
|
-
|
|
59
|
-
1. Click the "**New Page**" button that appears in development mode
|
|
60
|
-
2. Enter your route path (e.g., `/about`, `/blog/post`)
|
|
61
|
-
3. Press Enter - the route and EJS file are created automatically!
|
|
7
|
+
## What it does
|
|
62
8
|
|
|
63
|
-
|
|
64
|
-
Create an EJS file in `src/frontend/views/` and add the Express route manually.
|
|
9
|
+
Gardener lets you:
|
|
65
10
|
|
|
66
|
-
|
|
11
|
+
- Build UI using plain HTML, CSS, and JavaScript
|
|
12
|
+
- Convert DOM elements into reusable components
|
|
13
|
+
- Serve lightweight pages with minimal runtime overhead
|
|
67
14
|
|
|
68
|
-
|
|
15
|
+
---
|
|
69
16
|
|
|
70
|
-
|
|
71
|
-
Static components that render once:
|
|
72
|
-
```ejs
|
|
73
|
-
<%- include('partials/header') %>
|
|
74
|
-
```
|
|
17
|
+
## Why it exists
|
|
75
18
|
|
|
76
|
-
|
|
77
|
-
JSON-based components with the Gardener system:
|
|
78
|
-
|
|
79
|
-
**Creating a Component:**
|
|
80
|
-
1. Write your HTML structure in an EJS file or browser
|
|
81
|
-
2. In your JavaScript file, call:
|
|
82
|
-
```javascript
|
|
83
|
-
import { parser } from '/static/gardenerDev.js';
|
|
84
|
-
|
|
85
|
-
parser('.my-component-selector');
|
|
86
|
-
```
|
|
87
|
-
3. A window appears in the browser with the JSON representation
|
|
88
|
-
4. Name and save the component
|
|
89
|
-
5. The component is now reusable!
|
|
90
|
-
|
|
91
|
-
**Using a Component:**
|
|
92
|
-
```javascript
|
|
93
|
-
import { myComponent } from '/static/components/myComponent.js';
|
|
94
|
-
import { gardener, appendElement } from '/static/gardener.js';
|
|
95
|
-
|
|
96
|
-
// Render the component
|
|
97
|
-
const element = gardener(myComponent);
|
|
98
|
-
appendElement('#container', element);
|
|
99
|
-
```
|
|
19
|
+
Modern frameworks introduce significant abstraction and bundle size overhead.
|
|
100
20
|
|
|
101
|
-
|
|
102
|
-
```javascript
|
|
103
|
-
// Gardener components are JSON objects
|
|
104
|
-
export const button = {
|
|
105
|
-
t: 'button', // tag
|
|
106
|
-
cn: ['btn', 'primary'], // classNames
|
|
107
|
-
txt: 'Click me', // text content
|
|
108
|
-
attr: { id: 'submit' }, // attributes
|
|
109
|
-
events: { click: handleClick }, // event handlers
|
|
110
|
-
children: [...] // nested components
|
|
111
|
-
};
|
|
112
|
-
```
|
|
21
|
+
Gardener focuses on:
|
|
113
22
|
|
|
114
|
-
|
|
23
|
+
- Minimal runtime (~300 lines core)
|
|
24
|
+
- Direct DOM control
|
|
25
|
+
- Simplicity over abstraction
|
|
115
26
|
|
|
116
|
-
|
|
27
|
+
---
|
|
117
28
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
Define a component using JSON:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
{
|
|
35
|
+
"t": "div",
|
|
36
|
+
"cn": ["flex", "justify-center", "items-center", "target"],
|
|
37
|
+
"children": [
|
|
38
|
+
{
|
|
39
|
+
"t": "span",
|
|
40
|
+
"cn": ["text-xl", "font-bold"],
|
|
41
|
+
"txt": "Hello World"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"t": "button",
|
|
45
|
+
"txt": "click",
|
|
46
|
+
"events": {
|
|
47
|
+
"click": "() => console.log('button clicked')"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
126
50
|
]
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Use with parameters
|
|
130
|
-
const myCard = gardener(card, {
|
|
131
|
-
title: 'Hello',
|
|
132
|
-
description: 'World'
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### 5. Template System
|
|
137
|
-
|
|
138
|
-
**Save Current Page as Template:**
|
|
139
|
-
- Click "**Save Template**" button in the browser
|
|
140
|
-
- This saves the current page structure as a template
|
|
141
|
-
- Used automatically for deeper routes (e.g., `/blog/` template for `/blog/post-1`)
|
|
142
|
-
|
|
143
|
-
### 6. Dynamic Routes & Parameters
|
|
144
|
-
|
|
145
|
-
For parameterized routes like `/post/:id`:
|
|
146
|
-
|
|
147
|
-
**Backend:**
|
|
148
|
-
```javascript
|
|
149
|
-
app.get('/post/:id', (req, res) => {
|
|
150
|
-
res.render('post', {
|
|
151
|
-
id: req.params.id,
|
|
152
|
-
title: 'My Post'
|
|
153
|
-
});
|
|
154
|
-
});
|
|
51
|
+
}
|
|
155
52
|
```
|
|
156
53
|
|
|
157
|
-
|
|
158
|
-
```ejs
|
|
159
|
-
<h1>Post <%= id %></h1>
|
|
160
|
-
<p><%= title %></p>
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
**Query Parameters:**
|
|
164
|
-
```javascript
|
|
165
|
-
// Backend
|
|
166
|
-
app.get('/search', (req, res) => {
|
|
167
|
-
const query = req.query.q;
|
|
168
|
-
res.render('search', { query });
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Frontend EJS
|
|
172
|
-
<p>Searching for: <%= query %></p>
|
|
173
|
-
```
|
|
54
|
+
You don’t need to write JSON manually.
|
|
174
55
|
|
|
175
|
-
|
|
56
|
+
Instead:
|
|
176
57
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
- Perfect for rapid styling iterations
|
|
181
|
-
|
|
182
|
-
### 8. Image Optimization
|
|
183
|
-
|
|
184
|
-
Use the built-in image optimizer:
|
|
185
|
-
|
|
186
|
-
```html
|
|
187
|
-
<img src="/static/image_800x600.webp" alt="Optimized">
|
|
188
|
-
```
|
|
58
|
+
1. Write standard HTML
|
|
59
|
+
2. Select an element (id/class)
|
|
60
|
+
3. Run the parser → generates reusable component
|
|
189
61
|
|
|
190
|
-
|
|
62
|
+
---
|
|
191
63
|
|
|
192
|
-
|
|
193
|
-
- Converts to WebP format
|
|
194
|
-
- Resizes to specified dimensions
|
|
195
|
-
- Caches for performance
|
|
64
|
+
## Key Features
|
|
196
65
|
|
|
197
|
-
|
|
66
|
+
- Lightweight (~300 lines core, bundled with esbuild)
|
|
67
|
+
- No frontend framework dependency
|
|
68
|
+
- Reusable components (DOM → JSON)
|
|
69
|
+
- Server-side rendering via EJS
|
|
70
|
+
- Hot reloading
|
|
71
|
+
- Image optimization (Sharp + caching)
|
|
72
|
+
- Static build support
|
|
73
|
+
- One-command page creation
|
|
198
74
|
|
|
199
|
-
|
|
75
|
+
---
|
|
200
76
|
|
|
201
|
-
|
|
202
|
-
// Make a GET request
|
|
203
|
-
fetch('/createStatic')
|
|
204
|
-
```
|
|
77
|
+
## Architecture
|
|
205
78
|
|
|
206
|
-
|
|
79
|
+
Gardener follows a simple request → render → enhance flow:
|
|
207
80
|
|
|
208
|
-
|
|
81
|
+
- Express handles routing and backend logic
|
|
82
|
+
- EJS renders templates into HTML
|
|
83
|
+
- Frontend library enhances DOM with:
|
|
84
|
+
- component system
|
|
85
|
+
- state handling
|
|
86
|
+
- dev tooling
|
|
209
87
|
|
|
210
|
-
|
|
88
|
+
---
|
|
211
89
|
|
|
212
|
-
|
|
90
|
+
## Components
|
|
213
91
|
|
|
214
|
-
|
|
215
|
-
import {
|
|
216
|
-
gardener, // Convert JSON to DOM elements
|
|
217
|
-
fetchElement, // Query selector wrapper
|
|
218
|
-
appendElement, // Append child with error handling
|
|
219
|
-
createElement, // Create element with classes
|
|
220
|
-
insertText, // Set text content
|
|
221
|
-
replaceElement // Replace element in DOM
|
|
222
|
-
} from '/static/gardener.js';
|
|
92
|
+
### Two types:
|
|
223
93
|
|
|
224
|
-
|
|
225
|
-
const el = gardener({
|
|
226
|
-
t: 'div',
|
|
227
|
-
cn: ['container'],
|
|
228
|
-
attr: { id: 'main' },
|
|
229
|
-
children: [...]
|
|
230
|
-
});
|
|
231
|
-
```
|
|
94
|
+
1. Static Components
|
|
232
95
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
```javascript
|
|
236
|
-
import {
|
|
237
|
-
parser, // Convert DOM to JSON
|
|
238
|
-
parserWindow, // Show parser UI
|
|
239
|
-
State, // Reactive state management
|
|
240
|
-
addEl // Add event listener helper
|
|
241
|
-
} from '/static/gardenerDev.js';
|
|
242
|
-
|
|
243
|
-
// Reactive State
|
|
244
|
-
const count = new State(0);
|
|
245
|
-
count.registerCb((value) => {
|
|
246
|
-
console.log('Count:', value);
|
|
247
|
-
});
|
|
248
|
-
count.setTo(1); // Triggers callback
|
|
249
|
-
```
|
|
96
|
+
- EJS partials
|
|
97
|
+
- Require full page reload
|
|
250
98
|
|
|
251
|
-
|
|
99
|
+
2. Dynamic Components
|
|
252
100
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
nextPage, // Navigate with animation
|
|
256
|
-
nextPagehandler, // Set up link handlers
|
|
257
|
-
pageloader // Handle page loader
|
|
258
|
-
} from '/static/components/nonui/navigation.js';
|
|
259
|
-
```
|
|
101
|
+
- Generated from DOM → JSON
|
|
102
|
+
- Reusable and parameterized
|
|
260
103
|
|
|
261
|
-
|
|
104
|
+
Example parameter:
|
|
262
105
|
|
|
263
|
-
|
|
264
|
-
import { Fetch } from '/static/components/nonui/api.js';
|
|
106
|
+
<span>?title?</span>
|
|
265
107
|
|
|
266
|
-
|
|
267
|
-
const response = await Fetch('/api/data', { key: 'value' }, 'POST');
|
|
268
|
-
const data = await response.json();
|
|
269
|
-
```
|
|
108
|
+
This creates a dynamic variable "title".
|
|
270
109
|
|
|
271
|
-
|
|
110
|
+
---
|
|
272
111
|
|
|
273
|
-
|
|
274
|
-
your-app/
|
|
275
|
-
├── src/
|
|
276
|
-
│ ├── backend/
|
|
277
|
-
│ │ ├── server.ts # Main server file
|
|
278
|
-
│ │ ├── routes/ # Express routes
|
|
279
|
-
│ │ └── controllers/ # Route controllers
|
|
280
|
-
│ └── frontend/
|
|
281
|
-
│ ├── views/ # EJS templates
|
|
282
|
-
│ │ └── partials/ # EJS partials
|
|
283
|
-
│ ├── static/ # Client-side JS
|
|
284
|
-
│ │ ├── gardener.js # Core framework
|
|
285
|
-
│ │ ├── gardenerDev.js # Dev tools
|
|
286
|
-
│ │ ├── components/ # Gardener components
|
|
287
|
-
│ │ └── pages/ # Page-specific JS
|
|
288
|
-
│ ├── template/ # Page templates
|
|
289
|
-
│ ├── style.css # Compiled Tailwind
|
|
290
|
-
│ └── tailwind.css # Tailwind source
|
|
291
|
-
├── build/ # Static site output
|
|
292
|
-
└── package.json
|
|
293
|
-
```
|
|
112
|
+
## Image Optimization
|
|
294
113
|
|
|
295
|
-
|
|
114
|
+
- Uses Sharp for processing
|
|
115
|
+
- Cached images served via:
|
|
296
116
|
|
|
297
|
-
|
|
298
|
-
The `parser()` function converts any DOM element into a JSON representation that can be saved as a reusable component. This allows you to:
|
|
299
|
-
- Build UI visually in the browser
|
|
300
|
-
- Extract components without manual coding
|
|
301
|
-
- Create a library of reusable elements
|
|
117
|
+
/static/[image_name]_[width]x[height].webp
|
|
302
118
|
|
|
303
|
-
|
|
304
|
-
Components are composable JSON objects. Build complex UIs by nesting components:
|
|
119
|
+
---
|
|
305
120
|
|
|
306
|
-
|
|
307
|
-
const page = {
|
|
308
|
-
t: 'div',
|
|
309
|
-
children: [
|
|
310
|
-
header,
|
|
311
|
-
mainContent,
|
|
312
|
-
footer
|
|
313
|
-
]
|
|
314
|
-
};
|
|
315
|
-
```
|
|
121
|
+
## Hot Reloading
|
|
316
122
|
|
|
317
|
-
|
|
318
|
-
-
|
|
319
|
-
-
|
|
123
|
+
- Backend watches file changes
|
|
124
|
+
- Generates hash updates
|
|
125
|
+
- Frontend polls reload endpoint
|
|
126
|
+
- Full page reload triggered on change
|
|
320
127
|
|
|
321
|
-
|
|
128
|
+
---
|
|
322
129
|
|
|
323
|
-
|
|
130
|
+
## Tech Stack
|
|
324
131
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
132
|
+
- TypeScript
|
|
133
|
+
- Express
|
|
134
|
+
- EJS
|
|
135
|
+
- esbuild
|
|
136
|
+
- Sharp
|
|
328
137
|
|
|
329
|
-
|
|
138
|
+
---
|
|
330
139
|
|
|
331
|
-
|
|
140
|
+
## Setup
|
|
332
141
|
|
|
333
|
-
|
|
142
|
+
npm create gardener <project-name>
|
|
334
143
|
|
|
335
|
-
|
|
144
|
+
---
|
|
336
145
|
|
|
337
|
-
|
|
338
|
-
// 1. Create HTML in your EJS file
|
|
339
|
-
<div class="post">
|
|
340
|
-
<h2 class="title">?title?</h2>
|
|
341
|
-
<p class="content">?content?</p>
|
|
342
|
-
</div>
|
|
146
|
+
## Positioning
|
|
343
147
|
|
|
344
|
-
|
|
345
|
-
</div>
|
|
148
|
+
Gardener is designed for:
|
|
346
149
|
|
|
347
|
-
|
|
348
|
-
|
|
150
|
+
- Developers who prefer control over abstraction
|
|
151
|
+
- Lightweight applications
|
|
152
|
+
- Learning how frameworks work internally
|
|
349
153
|
|
|
350
|
-
|
|
154
|
+
---
|
|
351
155
|
|
|
352
|
-
|
|
156
|
+
## Future Work
|
|
353
157
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
158
|
+
- Partial DOM updates (instead of full reload)
|
|
159
|
+
- Better state management primitives
|
|
160
|
+
- Devtools / debugging layer
|
|
357
161
|
|
|
162
|
+
---
|
|
358
163
|
## 🤝 Contributing
|
|
359
164
|
|
|
360
165
|
Contributions are welcome! Visit the [GitHub repository](https://github.com/ritishDas/gardener).
|
|
@@ -373,3 +178,4 @@ MIT License - See LICENSE file for details.
|
|
|
373
178
|
---
|
|
374
179
|
|
|
375
180
|
Built with ❤️ for developers who want to move fast without breaking things.
|
|
181
|
+
|
package/package.json
CHANGED
package/template/.env
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Gardener — DOM-First Mini Framework
|
|
2
|
+
|
|
3
|
+
Gardener is a lightweight web framework built on top of Express and EJS. It enables building web applications without React or heavy frontend frameworks by using a DOM-first approach with reusable components.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Gardener lets you:
|
|
10
|
+
|
|
11
|
+
- Build UI using plain HTML, CSS, and JavaScript
|
|
12
|
+
- Convert DOM elements into reusable components
|
|
13
|
+
- Serve lightweight pages with minimal runtime overhead
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why it exists
|
|
18
|
+
|
|
19
|
+
Modern frameworks introduce significant abstraction and bundle size overhead.
|
|
20
|
+
|
|
21
|
+
Gardener focuses on:
|
|
22
|
+
|
|
23
|
+
- Minimal runtime (~300 lines core)
|
|
24
|
+
- Direct DOM control
|
|
25
|
+
- Simplicity over abstraction
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
Define a component using JSON:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
{
|
|
35
|
+
"t": "div",
|
|
36
|
+
"cn": ["flex", "justify-center", "items-center", "target"],
|
|
37
|
+
"children": [
|
|
38
|
+
{
|
|
39
|
+
"t": "span",
|
|
40
|
+
"cn": ["text-xl", "font-bold"],
|
|
41
|
+
"txt": "Hello World"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"t": "button",
|
|
45
|
+
"txt": "click",
|
|
46
|
+
"events": {
|
|
47
|
+
"click": "() => console.log('button clicked')"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You don’t need to write JSON manually.
|
|
55
|
+
|
|
56
|
+
Instead:
|
|
57
|
+
|
|
58
|
+
1. Write standard HTML
|
|
59
|
+
2. Select an element (id/class)
|
|
60
|
+
3. Run the parser → generates reusable component
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Key Features
|
|
65
|
+
|
|
66
|
+
- Lightweight (~300 lines core, bundled with esbuild)
|
|
67
|
+
- No frontend framework dependency
|
|
68
|
+
- Reusable components (DOM → JSON)
|
|
69
|
+
- Server-side rendering via EJS
|
|
70
|
+
- Hot reloading
|
|
71
|
+
- Image optimization (Sharp + caching)
|
|
72
|
+
- Static build support
|
|
73
|
+
- One-command page creation
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
Gardener follows a simple request → render → enhance flow:
|
|
80
|
+
|
|
81
|
+
- Express handles routing and backend logic
|
|
82
|
+
- EJS renders templates into HTML
|
|
83
|
+
- Frontend library enhances DOM with:
|
|
84
|
+
- component system
|
|
85
|
+
- state handling
|
|
86
|
+
- dev tooling
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Components
|
|
91
|
+
|
|
92
|
+
### Two types:
|
|
93
|
+
|
|
94
|
+
1. Static Components
|
|
95
|
+
|
|
96
|
+
- EJS partials
|
|
97
|
+
- Require full page reload
|
|
98
|
+
|
|
99
|
+
2. Dynamic Components
|
|
100
|
+
|
|
101
|
+
- Generated from DOM → JSON
|
|
102
|
+
- Reusable and parameterized
|
|
103
|
+
|
|
104
|
+
Example parameter:
|
|
105
|
+
|
|
106
|
+
<span>?title?</span>
|
|
107
|
+
|
|
108
|
+
This creates a dynamic variable "title".
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Image Optimization
|
|
113
|
+
|
|
114
|
+
- Uses Sharp for processing
|
|
115
|
+
- Cached images served via:
|
|
116
|
+
|
|
117
|
+
/static/[image_name]_[width]x[height].webp
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Hot Reloading
|
|
122
|
+
|
|
123
|
+
- Backend watches file changes
|
|
124
|
+
- Generates hash updates
|
|
125
|
+
- Frontend polls reload endpoint
|
|
126
|
+
- Full page reload triggered on change
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Tech Stack
|
|
131
|
+
|
|
132
|
+
- TypeScript
|
|
133
|
+
- Express
|
|
134
|
+
- EJS
|
|
135
|
+
- esbuild
|
|
136
|
+
- Sharp
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Setup
|
|
141
|
+
|
|
142
|
+
npm create gardener <project-name>
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Positioning
|
|
147
|
+
|
|
148
|
+
Gardener is designed for:
|
|
149
|
+
|
|
150
|
+
- Developers who prefer control over abstraction
|
|
151
|
+
- Lightweight applications
|
|
152
|
+
- Learning how frameworks work internally
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Future Work
|
|
157
|
+
|
|
158
|
+
- Partial DOM updates (instead of full reload)
|
|
159
|
+
- Better state management primitives
|
|
160
|
+
- Devtools / debugging layer
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
## 🤝 Contributing
|
|
164
|
+
|
|
165
|
+
Contributions are welcome! Visit the [GitHub repository](https://github.com/ritishDas/gardener).
|
|
166
|
+
|
|
167
|
+
## 📄 License
|
|
168
|
+
|
|
169
|
+
MIT License - See LICENSE file for details.
|
|
170
|
+
|
|
171
|
+
## 👤 Author
|
|
172
|
+
|
|
173
|
+
**ritishDas**
|
|
174
|
+
|
|
175
|
+
- GitHub: [@ritishDas](https://github.com/ritishDas)
|
|
176
|
+
- Website: [gardener.ritish.site](https://gardener.ritish.site)
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
Built with ❤️ for developers who want to move fast without breaking things.
|
|
181
|
+
|
|
@@ -14,14 +14,14 @@ const watchTarget = path.resolve('src', 'frontend');
|
|
|
14
14
|
let debounce: ReturnType<typeof setTimeout> | null = null;
|
|
15
15
|
|
|
16
16
|
fs.watch(watchTarget, { recursive: true }, (_event, filename) => {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Ignore hidden files and node_modules
|
|
18
|
+
if (!filename || filename.startsWith('.')) return;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
if (debounce) clearTimeout(debounce);
|
|
21
|
+
debounce = setTimeout(() => {
|
|
22
|
+
version = Date.now();
|
|
23
|
+
// console.log(`[gardener] file changed: ${filename} → version ${version}`);
|
|
24
|
+
}, 100);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
console.log(`[gardener] watching ${watchTarget} for changes…`);
|
|
@@ -30,5 +30,5 @@ console.log(`[gardener] watching ${watchTarget} for changes…`);
|
|
|
30
30
|
// GET /__gardener/hot-reload
|
|
31
31
|
// Returns { version: <number> }
|
|
32
32
|
export function hotReloadHandler(req: Request, res: Response) {
|
|
33
|
-
|
|
33
|
+
res.json({ version });
|
|
34
34
|
}
|
|
@@ -3,30 +3,87 @@ import fsp from "fs/promises";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import generateWebP from "../../libs/generateWebp.js";
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
import { fileURLToPath } from "url";
|
|
8
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
8
|
const __dirname = path.dirname(__filename);
|
|
10
9
|
|
|
10
|
+
// Whitelist of allowed remote image domains.
|
|
11
|
+
// Set GARDENER_IMAGE_DOMAINS=example.com,cdn.mysite.com in your .env
|
|
12
|
+
function getAllowedDomains(): string[] {
|
|
13
|
+
const raw = process.env.GARDENER_IMAGE_DOMAINS ?? "";
|
|
14
|
+
return raw
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((d) => d.trim().toLowerCase())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isAllowedUrl(rawUrl: string): { ok: true } | { ok: false; reason: string } {
|
|
21
|
+
let parsed: URL;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(rawUrl);
|
|
24
|
+
} catch {
|
|
25
|
+
return { ok: false, reason: "Malformed URL." };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
29
|
+
return { ok: false, reason: "Only http/https URLs are allowed." };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const allowedDomains = getAllowedDomains();
|
|
33
|
+
if (allowedDomains.length === 0) {
|
|
34
|
+
return { ok: false, reason: "No allowed image domains configured. Set GARDENER_IMAGE_DOMAINS in your .env" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
38
|
+
const isAllowed = allowedDomains.some(
|
|
39
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!isAllowed) {
|
|
43
|
+
return { ok: false, reason: `Domain '${hostname}' is not in the allowed list.` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function downloadRemoteImage(
|
|
50
|
+
remoteUrl: string,
|
|
51
|
+
destPath: string
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const response = await fetch(remoteUrl);
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Remote fetch failed: ${response.status} ${response.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
60
|
+
if (!contentType.startsWith("image/")) {
|
|
61
|
+
throw new Error(`Remote URL did not return an image (got: ${contentType})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const buffer = await response.arrayBuffer();
|
|
65
|
+
await fsp.writeFile(destPath, Buffer.from(buffer));
|
|
66
|
+
}
|
|
11
67
|
|
|
12
68
|
export async function imageOptimiser(req: Request, res: Response) {
|
|
13
69
|
try {
|
|
14
70
|
const { name } = req.params;
|
|
15
71
|
|
|
16
|
-
if (typeof name !==
|
|
17
|
-
|
|
18
|
-
|
|
72
|
+
if (typeof name !== "string") {
|
|
73
|
+
return res.status(400).json({ success: false, message: "invalid path" });
|
|
74
|
+
}
|
|
19
75
|
|
|
76
|
+
// name format: baseName_{width}x{height}.webp
|
|
77
|
+
const match = name.match(/^(.+?)_(\d+)x(\d+)\.webp$/);
|
|
20
78
|
if (!match) {
|
|
21
|
-
return res.status(400).json({ error: "Invalid image format" });
|
|
79
|
+
return res.status(400).json({ error: "Invalid image format. Expected: name_{w}x{h}.webp" });
|
|
22
80
|
}
|
|
23
81
|
|
|
24
82
|
const [, baseName, widthStr, heightStr] = match;
|
|
83
|
+
const width = parseInt(widthStr!, 10);
|
|
84
|
+
const height = parseInt(heightStr!, 10);
|
|
25
85
|
|
|
26
|
-
if
|
|
27
|
-
const width = parseInt(widthStr, 10);
|
|
28
|
-
const height = parseInt(heightStr, 10);
|
|
29
|
-
|
|
86
|
+
// ── 1. Serve from cache if already converted ────────────────────────────
|
|
30
87
|
const cacheDir = path.join(__dirname, "..", "..", "..", "frontend", "static", "cache");
|
|
31
88
|
await fsp.mkdir(cacheDir, { recursive: true });
|
|
32
89
|
|
|
@@ -34,34 +91,66 @@ export async function imageOptimiser(req: Request, res: Response) {
|
|
|
34
91
|
|
|
35
92
|
try {
|
|
36
93
|
await fsp.access(outputPath);
|
|
37
|
-
return res.sendFile(path.basename(outputPath), {
|
|
38
|
-
root: path.dirname(outputPath),
|
|
39
|
-
});
|
|
94
|
+
return res.sendFile(path.basename(outputPath), { root: path.dirname(outputPath) });
|
|
40
95
|
} catch {
|
|
41
96
|
// not cached → continue
|
|
42
97
|
}
|
|
43
98
|
|
|
44
|
-
|
|
45
|
-
const
|
|
99
|
+
// ── 2. Locate source image in assets/ (local) ───────────────────────────
|
|
100
|
+
const assetsDir = path.resolve(__dirname, "..", "..", "..", "frontend", "assets");
|
|
101
|
+
await fsp.mkdir(assetsDir, { recursive: true });
|
|
46
102
|
|
|
47
|
-
|
|
48
|
-
const parsed = path.parse(file);
|
|
49
|
-
return parsed.name === baseName;
|
|
50
|
-
});
|
|
103
|
+
let inputPath: string | null = null;
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
const localFiles = await fsp.readdir(assetsDir);
|
|
106
|
+
const localMatch = localFiles.find((file) => path.parse(file).name === baseName);
|
|
107
|
+
if (localMatch) {
|
|
108
|
+
inputPath = path.join(assetsDir, localMatch);
|
|
54
109
|
}
|
|
55
110
|
|
|
56
|
-
|
|
111
|
+
// ── 3. Check assets/remote/ if not found locally ────────────────────────
|
|
112
|
+
const remoteAssetsDir = path.join(assetsDir, "remote");
|
|
113
|
+
await fsp.mkdir(remoteAssetsDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
if (!inputPath) {
|
|
116
|
+
const remoteFiles = await fsp.readdir(remoteAssetsDir);
|
|
117
|
+
const remoteMatch = remoteFiles.find((file) => path.parse(file).name === baseName);
|
|
118
|
+
if (remoteMatch) {
|
|
119
|
+
inputPath = path.join(remoteAssetsDir, remoteMatch);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 4. Download from remote URL if provided and still not found ──────────
|
|
124
|
+
if (!inputPath) {
|
|
125
|
+
const remoteUrl = req.query.url;
|
|
126
|
+
|
|
127
|
+
if (typeof remoteUrl !== "string" || !remoteUrl) {
|
|
128
|
+
return res.status(404).json({
|
|
129
|
+
error: "Source image not found. Provide ?url=<imageUrl> to use a remote source.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const urlCheck = isAllowedUrl(remoteUrl);
|
|
134
|
+
if (!urlCheck.ok) {
|
|
135
|
+
return res.status(400).json({ error: urlCheck.reason });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Infer extension from URL (fallback to .jpg)
|
|
139
|
+
const urlPathname = new URL(remoteUrl).pathname;
|
|
140
|
+
const remoteExt = path.extname(urlPathname) || ".jpg";
|
|
141
|
+
const destFilename = `${baseName}${remoteExt}`;
|
|
142
|
+
const destPath = path.join(remoteAssetsDir, destFilename);
|
|
143
|
+
|
|
144
|
+
await downloadRemoteImage(remoteUrl, destPath);
|
|
145
|
+
inputPath = destPath;
|
|
146
|
+
}
|
|
57
147
|
|
|
148
|
+
// ── 5. Convert & cache ───────────────────────────────────────────────────
|
|
58
149
|
await generateWebP(inputPath, outputPath, width, height);
|
|
59
150
|
|
|
60
|
-
return res.sendFile(path.basename(outputPath), {
|
|
61
|
-
root: path.dirname(outputPath),
|
|
62
|
-
});
|
|
151
|
+
return res.sendFile(path.basename(outputPath), { root: path.dirname(outputPath) });
|
|
63
152
|
} catch (err) {
|
|
64
153
|
console.error(err);
|
|
65
|
-
return res.status(500).json({ error: "Image optimisation failed" });
|
|
154
|
+
return res.status(500).json({ error: "Image optimisation failed", detail: String(err) });
|
|
66
155
|
}
|
|
67
156
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Request, Response } from 'express';
|
|
2
2
|
import { Router } from "express";
|
|
3
|
-
import { addComponent, addPage, imageOptimiser, saveTemplate } from "../controllers/gardener/index.js";
|
|
4
|
-
import { hotReloadHandler } from "../controllers/gardener/hotReload.js";
|
|
3
|
+
import { addComponent, addPage, imageOptimiser, saveTemplate, hotReloadHandler } from "../controllers/gardener/index.js";
|
|
5
4
|
|
|
6
5
|
const router: Router = Router();
|
|
7
6
|
export default router;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// server.ts
|
|
2
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
3
|
import 'dotenv/config';
|
|
3
4
|
import express from 'express';
|
|
4
5
|
import frontendRoute from './routes/gardener.route.js'
|
|
6
|
+
|
|
5
7
|
import path from "path";
|
|
6
8
|
|
|
7
9
|
const app = express();
|
|
@@ -15,10 +17,26 @@ const staticFiles = path.resolve(__dirname, '..', 'frontend')
|
|
|
15
17
|
|
|
16
18
|
app.set('views', path.join(staticFiles, 'views'));
|
|
17
19
|
app.set("view engine", "ejs");
|
|
18
|
-
app.use(express.static(staticFiles
|
|
20
|
+
app.use(express.static(staticFiles,
|
|
21
|
+
{
|
|
22
|
+
maxAge: '1y', // 1 year
|
|
23
|
+
immutable: true
|
|
24
|
+
}
|
|
25
|
+
));
|
|
26
|
+
|
|
19
27
|
app.use(express.json());
|
|
20
28
|
app.use(frontendRoute);
|
|
21
29
|
|
|
30
|
+
|
|
31
|
+
app.use((err: Error & { status: number }, req: Request, res: Response, next: NextFunction) => {
|
|
32
|
+
console.error(err.stack);
|
|
33
|
+
|
|
34
|
+
res.status(err.status || 500).json({
|
|
35
|
+
success: false,
|
|
36
|
+
message: err.message || "Internal Server Error",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
22
40
|
const PORT = process.env.PORT || 3000;
|
|
23
41
|
|
|
24
42
|
app.listen(PORT, () => {
|
|
Binary file
|
|
@@ -271,9 +271,6 @@
|
|
|
271
271
|
.right-4 {
|
|
272
272
|
right: calc(var(--spacing) * 4);
|
|
273
273
|
}
|
|
274
|
-
.bottom-0 {
|
|
275
|
-
bottom: calc(var(--spacing) * 0);
|
|
276
|
-
}
|
|
277
274
|
.bottom-4 {
|
|
278
275
|
bottom: calc(var(--spacing) * 4);
|
|
279
276
|
}
|
|
@@ -307,9 +304,6 @@
|
|
|
307
304
|
.z-90 {
|
|
308
305
|
z-index: 90;
|
|
309
306
|
}
|
|
310
|
-
.z-100 {
|
|
311
|
-
z-index: 100;
|
|
312
|
-
}
|
|
313
307
|
.z-\[100\] {
|
|
314
308
|
z-index: 100;
|
|
315
309
|
}
|
|
@@ -505,9 +499,6 @@
|
|
|
505
499
|
.rounded-lg {
|
|
506
500
|
border-radius: var(--radius-lg);
|
|
507
501
|
}
|
|
508
|
-
.rounded-md {
|
|
509
|
-
border-radius: var(--radius-md);
|
|
510
|
-
}
|
|
511
502
|
.rounded-xl {
|
|
512
503
|
border-radius: var(--radius-xl);
|
|
513
504
|
}
|
|
@@ -531,10 +522,6 @@
|
|
|
531
522
|
border-top-style: var(--tw-border-style);
|
|
532
523
|
border-top-width: 4px;
|
|
533
524
|
}
|
|
534
|
-
.border-b-1 {
|
|
535
|
-
border-bottom-style: var(--tw-border-style);
|
|
536
|
-
border-bottom-width: 1px;
|
|
537
|
-
}
|
|
538
525
|
.border-l-8 {
|
|
539
526
|
border-left-style: var(--tw-border-style);
|
|
540
527
|
border-left-width: 8px;
|
|
@@ -753,9 +740,6 @@
|
|
|
753
740
|
.text-green-400 {
|
|
754
741
|
color: var(--color-green-400);
|
|
755
742
|
}
|
|
756
|
-
.text-green-500 {
|
|
757
|
-
color: var(--color-green-500);
|
|
758
|
-
}
|
|
759
743
|
.text-green-800 {
|
|
760
744
|
color: var(--color-green-800);
|
|
761
745
|
}
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<body class="bg-slate-50 text-slate-900 font-sans">
|
|
14
14
|
|
|
15
|
-
<div class='hidden' id='fileName'>
|
|
16
|
-
<%=fileName%>
|
|
17
|
-
</div>
|
|
15
|
+
<div class='hidden' id='fileName'><%=fileName%></div>
|
|
18
16
|
<div class='loader w-screen h-screen bg-white fixed z-2'> </div>
|
|
19
17
|
<div class='notification'></div>
|
|
20
18
|
|
|
@@ -43,7 +41,7 @@
|
|
|
43
41
|
href='https://www.npmjs.com/package/create-gardener'>View On npm</a>
|
|
44
42
|
</div>
|
|
45
43
|
</div>
|
|
46
|
-
<img src="/static/cache/
|
|
44
|
+
<img src="/static/cache/betterway_200x200.webp?url=https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQIc-V0t9zqjdPUe6IoI4M7U4jSW6yY6dhhtw&s" alt="Gardener Logo" class="w-96 h-96 object-contain ml-10">
|
|
47
45
|
</div>
|
|
48
46
|
|
|
49
47
|
<!-- Copy command -->
|
|
@@ -136,4 +134,4 @@ gardener({
|
|
|
136
134
|
<script type='module' src='/static/pages/pages._.js'></script>
|
|
137
135
|
<% } %>
|
|
138
136
|
|
|
139
|
-
</html>
|
|
137
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|