create-gardener 1.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/.envrc +1 -0
- package/LICENSE +9 -0
- package/Readme.md +349 -0
- package/flake.lock +61 -0
- package/flake.nix +23 -0
- package/package.json +34 -0
- package/starter.js +73 -0
- package/template/Readme.md +349 -0
- package/template/package.json +34 -0
- package/template/src/backend/cache/w_500x500.webp +0 -0
- package/template/src/backend/controllers/gardener.controller.ts +211 -0
- package/template/src/backend/frontendtemplate.ejs +26 -0
- package/template/src/backend/libs/generateWebp.ts +26 -0
- package/template/src/backend/routes/gardener.route.ts +19 -0
- package/template/src/backend/server.ts +30 -0
- package/template/src/frontend/assets/w.webp +0 -0
- package/template/src/frontend/components/test.js +54 -0
- package/template/src/frontend/gardener.js +404 -0
- package/template/src/frontend/global.js +57 -0
- package/template/src/frontend/style.css +481 -0
- package/template/src/frontend/tailwind.css +1 -0
- package/template/src/frontend/views/_.ejs +26 -0
- package/template/src/frontend/views/partials/loader.ejs +3 -0
- package/template/tailwind.config.js +0 -0
- package/template/tsconfig.json +43 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Gardener 🌱
|
|
2
|
+
|
|
3
|
+
**Gardener** is a small development toolkit and micro-framework for building websites with **declarative DOM JSON**, server-rendered templates, and a **custom static site generation pipeline**.
|
|
4
|
+
|
|
5
|
+
It is designed for developers who want:
|
|
6
|
+
|
|
7
|
+
* full control over HTML structure
|
|
8
|
+
* minimal abstractions
|
|
9
|
+
* a fast local dev experience
|
|
10
|
+
* a deterministic static output for production
|
|
11
|
+
|
|
12
|
+
Gardener sits somewhere between a tiny framework and a build system.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What Gardener Includes
|
|
17
|
+
|
|
18
|
+
### Core
|
|
19
|
+
|
|
20
|
+
* 🌿 **gardener.js** — declarative DOM builder using JSON objects
|
|
21
|
+
* 🔁 **parser** — convert real DOM elements back into gardener-compatible JSON
|
|
22
|
+
* 📄 **EJS** for simple server-rendered views
|
|
23
|
+
* 🎨 **Tailwind CSS** for fast styling
|
|
24
|
+
|
|
25
|
+
### Dev Server
|
|
26
|
+
|
|
27
|
+
* Express-based development server
|
|
28
|
+
* Hot reload toggle (Ctrl + H)
|
|
29
|
+
* Endpoints to create pages and components at runtime (dev convenience)
|
|
30
|
+
|
|
31
|
+
### Images
|
|
32
|
+
|
|
33
|
+
* Deterministic image optimization endpoint
|
|
34
|
+
* Sharp-powered resize + WebP conversion
|
|
35
|
+
* Filesystem cache reused during static builds
|
|
36
|
+
|
|
37
|
+
### Static Site Generation (SSG)
|
|
38
|
+
|
|
39
|
+
* Render EJS views into HTML
|
|
40
|
+
* Convert route-encoded filenames into nested directories
|
|
41
|
+
* Merge frontend assets and image cache
|
|
42
|
+
* Clean temporary build artifacts
|
|
43
|
+
* Produce a deployable static directory
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Project Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/
|
|
51
|
+
├── backend/
|
|
52
|
+
│ ├── routes/
|
|
53
|
+
│ ├── controllers/
|
|
54
|
+
│ ├── libs/
|
|
55
|
+
│ ├── cache/ # generated image cache (build artifact)
|
|
56
|
+
│ └── server.ts
|
|
57
|
+
│
|
|
58
|
+
├── frontend/
|
|
59
|
+
│ ├── views/ # EJS templates (source)
|
|
60
|
+
│ ├── assets/ # original images
|
|
61
|
+
│ ├── components/
|
|
62
|
+
│ ├── gardener.js
|
|
63
|
+
│ └── styles/
|
|
64
|
+
│
|
|
65
|
+
├── frontendStatic/ # final static output (generated)
|
|
66
|
+
└── tempfrontend/ # temporary build output (deleted after build)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
* Node.js v16+ (v18+ recommended)
|
|
74
|
+
* pnpm (recommended) or npm
|
|
75
|
+
* Optional: PostgreSQL
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quickstart (Development)
|
|
80
|
+
|
|
81
|
+
### 1. Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/ritishDas/Gardener.git
|
|
85
|
+
cd Gardener
|
|
86
|
+
pnpm install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2. Run dev server
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pnpm run dev
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
* Server runs at **[http://localhost:3000](http://localhost:3000)**
|
|
96
|
+
* Tailwind watcher and TypeScript server run together
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Image Optimization & Caching
|
|
101
|
+
|
|
102
|
+
Gardener provides a **deterministic image optimization endpoint**.
|
|
103
|
+
|
|
104
|
+
### Route
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
GET /cache/:name
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Filename format
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
<basename>_<width>x<height>.webp
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Example
|
|
117
|
+
|
|
118
|
+
```http
|
|
119
|
+
GET /cache/hero_500x300.webp
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
HTML usage:
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<img src="/cache/hero_500x300.webp" alt="hero" />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### How it works
|
|
131
|
+
|
|
132
|
+
1. Parses filename to extract:
|
|
133
|
+
|
|
134
|
+
* base name
|
|
135
|
+
* width
|
|
136
|
+
* height
|
|
137
|
+
2. Checks cache:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
src/backend/cache/
|
|
141
|
+
```
|
|
142
|
+
3. If cached → return immediately
|
|
143
|
+
4. If not cached:
|
|
144
|
+
|
|
145
|
+
* Finds source image in:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
src/frontend/assets/
|
|
149
|
+
```
|
|
150
|
+
* Resizes and converts to WebP (Sharp)
|
|
151
|
+
* Stores result in cache
|
|
152
|
+
5. Serves the optimized image
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### Static Build Integration
|
|
157
|
+
|
|
158
|
+
During static generation:
|
|
159
|
+
|
|
160
|
+
* All cached images under:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
src/backend/cache/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
are copied into:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
src/frontendStatic/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Static HTML can safely reference:
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<img src="/cache/hero_500x300.webp" />
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
No runtime image processing is required in production.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Static Site Generation
|
|
183
|
+
|
|
184
|
+
Gardener includes a custom static build pipeline.
|
|
185
|
+
|
|
186
|
+
### What it does
|
|
187
|
+
|
|
188
|
+
1. Renders EJS views into HTML
|
|
189
|
+
2. Writes temporary files using route-encoded filenames
|
|
190
|
+
(example: `_blog_posts_hello.html`)
|
|
191
|
+
3. Converts them into directory-based routes:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
blog/posts/hello/index.html
|
|
195
|
+
```
|
|
196
|
+
4. Copies frontend assets
|
|
197
|
+
5. Copies image cache
|
|
198
|
+
6. Deletes temporary build directory
|
|
199
|
+
|
|
200
|
+
### Output
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
src/frontendStatic/
|
|
204
|
+
├── index.html
|
|
205
|
+
├── blog/
|
|
206
|
+
│ └── posts/
|
|
207
|
+
│ └── hello/
|
|
208
|
+
│ └── index.html
|
|
209
|
+
├── assets/
|
|
210
|
+
└── cache/
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
This directory is ready for:
|
|
214
|
+
|
|
215
|
+
* static hosting
|
|
216
|
+
* CDN deployment
|
|
217
|
+
* Nginx / Caddy / Netlify / Vercel
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Frontend API — `gardener.js`
|
|
222
|
+
|
|
223
|
+
File: `src/frontend/gardener.js`
|
|
224
|
+
|
|
225
|
+
### `gardener(obj)`
|
|
226
|
+
|
|
227
|
+
Create DOM elements from JSON.
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
const el = gardener({
|
|
231
|
+
t: 'div',
|
|
232
|
+
cn: ['card', 'p-4'],
|
|
233
|
+
children: [
|
|
234
|
+
{ t: 'h2', txt: 'Title' },
|
|
235
|
+
{ t: 'p', txt: 'Content' }
|
|
236
|
+
]
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
document.body.appendChild(el);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### `parser(elementOrHtml, isParent = true)`
|
|
245
|
+
|
|
246
|
+
Convert DOM into gardener JSON.
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
const json = parser(document.querySelector('.hero'));
|
|
250
|
+
console.log(json);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### `parserWindow(text)`
|
|
256
|
+
|
|
257
|
+
Dev-only UI:
|
|
258
|
+
|
|
259
|
+
* Preview parsed JSON
|
|
260
|
+
* Press **Y** to create a component file
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Utilities
|
|
265
|
+
|
|
266
|
+
* `imagePreloader(images)`
|
|
267
|
+
* `fetchElement(selector)`
|
|
268
|
+
* `appendElement(parent, child)`
|
|
269
|
+
* `replaceElement(original, newElem)`
|
|
270
|
+
* `createElement(type, classname)`
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Dev-Only Endpoints ⚠️
|
|
275
|
+
|
|
276
|
+
### `POST /addcomponent`
|
|
277
|
+
|
|
278
|
+
Creates a frontend component file.
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"path": "components/MyComp.js",
|
|
283
|
+
"component": "{ t: 'div', txt: 'Hello' }"
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Writes directly to the filesystem.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `POST /addpage`
|
|
292
|
+
|
|
293
|
+
Creates an EJS page and registers a route.
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{ "page": "/my-page" }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
* Generates a new EJS file
|
|
300
|
+
* Appends a route to the backend router
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Security Notes
|
|
305
|
+
|
|
306
|
+
⚠️ **Important**
|
|
307
|
+
|
|
308
|
+
* `/addcomponent` and `/addpage` mutate files and routes
|
|
309
|
+
* Intended for **local development only**
|
|
310
|
+
* Do NOT expose publicly without authentication and sanitization
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Troubleshooting
|
|
315
|
+
|
|
316
|
+
* **Server not starting**
|
|
317
|
+
|
|
318
|
+
* Check `.env`
|
|
319
|
+
* Ensure PostgreSQL is running (if enabled)
|
|
320
|
+
* Inspect logs from `pnpm run dev`
|
|
321
|
+
|
|
322
|
+
* **Images not loading**
|
|
323
|
+
|
|
324
|
+
* Ensure source image exists in `src/frontend/assets`
|
|
325
|
+
* Filename must match `<name>_<width>x<height>.webp`
|
|
326
|
+
|
|
327
|
+
* **Tailwind not updating**
|
|
328
|
+
|
|
329
|
+
* Ensure `pnpm install` completed successfully
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
This is a small personal/dev-focused toolkit.
|
|
336
|
+
|
|
337
|
+
Contributions are welcome:
|
|
338
|
+
|
|
339
|
+
* bug fixes
|
|
340
|
+
* documentation improvements
|
|
341
|
+
* build pipeline enhancements
|
|
342
|
+
* security hardening
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
MIT
|
|
349
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-gardener",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A dom gardener converting dom elements into json and vice versa",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin":{
|
|
7
|
+
"create-gardener":"./starter.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"dev": "concurrently \"tsx watch src/backend/server.ts\" \"tailwindcss -w -i src/frontend/tailwind.css -o src/frontend/style.css\""
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "ritishDas",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"packageManager": "pnpm@10.20.0",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@types/ejs": "^3.1.5",
|
|
20
|
+
"dotenv": "^17.2.3",
|
|
21
|
+
"ejs": "^3.1.10",
|
|
22
|
+
"express": "^5.2.1",
|
|
23
|
+
"sharp": "^0.34.5",
|
|
24
|
+
"tailwindcss": "^4.1.18",
|
|
25
|
+
"types": "^0.1.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/express": "^5.0.6",
|
|
29
|
+
"@types/node": "^25.0.2",
|
|
30
|
+
"concurrently": "^9.2.1",
|
|
31
|
+
"tsx": "^4.21.0",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import fs, { readFile, readFileSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import ejs from "ejs";
|
|
5
|
+
import fsp from "fs/promises";
|
|
6
|
+
import generateWebP from "../libs/generateWebp.js";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
const availableCache: Record<string, boolean> = {};
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
interface AddComponentBody {
|
|
15
|
+
path: string;
|
|
16
|
+
component: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addComponent(req: Request<{}, {}, AddComponentBody>, res: Response) {
|
|
20
|
+
try {
|
|
21
|
+
const { path: filePath, component } = req.body;
|
|
22
|
+
const parsed = JSON.parse(component);
|
|
23
|
+
|
|
24
|
+
// pretty format (2 spaces)
|
|
25
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
26
|
+
|
|
27
|
+
const filecontent = `
|
|
28
|
+
import { gardener } from '../gardener.js'
|
|
29
|
+
|
|
30
|
+
export default function () {
|
|
31
|
+
return gardener(${formatted})
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync(`./src/frontend/${filePath}`, filecontent, "utf8");
|
|
36
|
+
|
|
37
|
+
res.json({ success: true });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const error = err as Error;
|
|
40
|
+
res.json({ success: false, msg: error.message });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
export async function imageOptimiser(req: Request, res: Response) {
|
|
46
|
+
try {
|
|
47
|
+
const { name } = req.params;
|
|
48
|
+
|
|
49
|
+
if (!name) return;
|
|
50
|
+
// name format: test_500x300.webp
|
|
51
|
+
const match = name.match(/^(.+?)_(\d+)x(\d+)\.webp$/);
|
|
52
|
+
|
|
53
|
+
if (!match) {
|
|
54
|
+
return res.status(400).json({ error: "Invalid image format" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [, baseName, widthStr, heightStr] = match;
|
|
58
|
+
|
|
59
|
+
if (!widthStr || !heightStr) return;
|
|
60
|
+
const width = parseInt(widthStr, 10);
|
|
61
|
+
const height = parseInt(heightStr, 10);
|
|
62
|
+
|
|
63
|
+
const cacheDir = path.join(__dirname, "../cache");
|
|
64
|
+
await fsp.mkdir(cacheDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
const outputPath = path.join(cacheDir, name);
|
|
67
|
+
|
|
68
|
+
// 1️⃣ Return cached file if exists
|
|
69
|
+
try {
|
|
70
|
+
await fsp.access(outputPath);
|
|
71
|
+
return res.sendFile(path.basename(outputPath), {
|
|
72
|
+
root: path.dirname(outputPath),
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
// not cached → continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2️⃣ Find source image with same base name
|
|
79
|
+
const assetsDir = path.resolve("./src/frontend/assets");
|
|
80
|
+
const files = await fsp.readdir(assetsDir);
|
|
81
|
+
|
|
82
|
+
const sourceFile = files.find((file) => {
|
|
83
|
+
const parsed = path.parse(file);
|
|
84
|
+
return parsed.name === baseName;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!sourceFile) {
|
|
88
|
+
return res.status(404).json({ error: "Source image not found" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const inputPath = path.join(assetsDir, sourceFile);
|
|
92
|
+
|
|
93
|
+
// 3️⃣ Generate optimized WebP
|
|
94
|
+
await generateWebP(inputPath, outputPath, width, height);
|
|
95
|
+
|
|
96
|
+
// 4️⃣ Return generated file
|
|
97
|
+
return res.sendFile(path.basename(outputPath), {
|
|
98
|
+
root: path.dirname(outputPath),
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(err);
|
|
102
|
+
return res.status(500).json({ error: "Image optimisation failed" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function addPage(req: Request, res: Response) {
|
|
107
|
+
try {
|
|
108
|
+
const pagename: string = req.body.page;
|
|
109
|
+
const buffer = readFileSync('./src/backend/frontendtemplate.ejs', 'utf8');
|
|
110
|
+
const name = pagename.replaceAll('/', '_');
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(`./src/frontend/views/${name}.ejs`, buffer, "utf8");
|
|
113
|
+
|
|
114
|
+
fs.appendFileSync('./src/backend/routes/gardener.route.ts', ` router.route("${pagename}").get((req, res) => res.render("${name}"))\n `);
|
|
115
|
+
|
|
116
|
+
res.json({ success: true });
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const error = err as Error;
|
|
120
|
+
res.json({ success: false, msg: error.message });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
export async function createStatic(req: Request, res: Response) {
|
|
127
|
+
try {
|
|
128
|
+
const viewsDir = path.resolve("src/frontend/views");
|
|
129
|
+
const outDir = path.resolve("src/tempfrontend");
|
|
130
|
+
const finalOut = path.resolve("src/frontendStatic");
|
|
131
|
+
|
|
132
|
+
const otherAssets = path.resolve("src/frontend");
|
|
133
|
+
await fsp.mkdir(outDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const entries2 = await fsp.readdir(otherAssets, { withFileTypes: true });
|
|
136
|
+
const entries = await fsp.readdir(viewsDir, { withFileTypes: true });
|
|
137
|
+
|
|
138
|
+
const rendered: string[] = [];
|
|
139
|
+
|
|
140
|
+
for (const entry of entries2) {
|
|
141
|
+
if (!entry.isFile()) continue;
|
|
142
|
+
const srcPath = path.join(otherAssets, entry.name);
|
|
143
|
+
const outputPath = path.join(finalOut, entry.name);
|
|
144
|
+
|
|
145
|
+
await fsp.copyFile(srcPath, outputPath);
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
// skip folders (partials, layouts, etc.)
|
|
151
|
+
if (!entry.isFile()) continue;
|
|
152
|
+
if (!entry.name.endsWith(".ejs")) continue;
|
|
153
|
+
|
|
154
|
+
const inputPath = path.join(viewsDir, entry.name);
|
|
155
|
+
const outputName = entry.name.replace(/\.ejs$/, ".html");
|
|
156
|
+
const outputPath = path.join(outDir, outputName);
|
|
157
|
+
|
|
158
|
+
const html = await ejs.renderFile(
|
|
159
|
+
inputPath,
|
|
160
|
+
{
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
// async: true,
|
|
164
|
+
views: [viewsDir], // needed for includes
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await fsp.writeFile(outputPath, html, "utf8");
|
|
169
|
+
rendered.push(outputName);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const entries3 = await fsp.readdir(outDir, { withFileTypes: true });
|
|
173
|
+
for (const entry of entries3) {
|
|
174
|
+
|
|
175
|
+
// "_path1_path2_path3.html" -> ["path1", "path2", "path3"]
|
|
176
|
+
const parts = entry.name
|
|
177
|
+
.replace(/^_/, "")
|
|
178
|
+
.replace(/\.html$/, "")
|
|
179
|
+
.split("_");
|
|
180
|
+
|
|
181
|
+
const targetDir = path.join(finalOut, ...parts);
|
|
182
|
+
const targetFile = path.join(targetDir, "index.html");
|
|
183
|
+
|
|
184
|
+
// ensure directories exist
|
|
185
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
186
|
+
console.log('done');
|
|
187
|
+
// copy file
|
|
188
|
+
await fsp.copyFile(path.join(outDir, entry.name), targetFile);
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
await fsp.rm(outDir, { recursive: true, force: true });
|
|
192
|
+
await fsp.cp(
|
|
193
|
+
path.resolve("src/frontend/components"),
|
|
194
|
+
path.join(finalOut, 'components'),
|
|
195
|
+
{ recursive: true }
|
|
196
|
+
);
|
|
197
|
+
await fsp.cp(
|
|
198
|
+
path.resolve("src/backend/cache"),
|
|
199
|
+
path.join(finalOut, 'cache'),
|
|
200
|
+
{ recursive: true }
|
|
201
|
+
);
|
|
202
|
+
return res.json({
|
|
203
|
+
success: true,
|
|
204
|
+
generated: rendered,
|
|
205
|
+
outDir,
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error(err);
|
|
209
|
+
return res.status(500).json({ error: "Static build failed" });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<link rel="stylesheet" href="/style.css">
|
|
7
|
+
<title>Gardener</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id='body'>
|
|
11
|
+
<%- include('partials/loader') %>
|
|
12
|
+
<div class="hero flex justify-around items-center p-5 h-[90vh]">
|
|
13
|
+
<p class='p-5'>
|
|
14
|
+
Gardener is a front-end library for creating and manipulating DOM elements using a declarative JavaScript object syntax. It includes a development server with features like hot-reloading and on-the-fly component creation from existing HTML. The server also provides dynamic image resizing and caching.
|
|
15
|
+
</p>
|
|
16
|
+
<img src="/cache/w_500x500.webp" alt="logo" class="w-500">
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
<script src='/global.js' type="module"></script>
|
|
21
|
+
<script type="module">
|
|
22
|
+
|
|
23
|
+
</script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fsp from "fs/promises";
|
|
3
|
+
import sharp from "sharp";
|
|
4
|
+
|
|
5
|
+
export default async function generateWebP(
|
|
6
|
+
inputPath: string,
|
|
7
|
+
outputPath: string,
|
|
8
|
+
width: number,
|
|
9
|
+
height: number
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const cacheDir = path.dirname(outputPath);
|
|
12
|
+
await fsp.mkdir(cacheDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
console.log(`Processing image: ${inputPath}`);
|
|
15
|
+
console.log(`Output path: ${outputPath}`);
|
|
16
|
+
|
|
17
|
+
await sharp(inputPath)
|
|
18
|
+
.resize(width, height, {
|
|
19
|
+
fit: "inside",
|
|
20
|
+
withoutEnlargement: true,
|
|
21
|
+
})
|
|
22
|
+
.webp({ quality: 100 })
|
|
23
|
+
.toFile(outputPath);
|
|
24
|
+
|
|
25
|
+
console.log("✅ Image successfully generated");
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Router } from "express"
|
|
2
|
+
import { addComponent, addPage, createStatic, imageOptimiser } from "../controllers/gardener.controller.js";
|
|
3
|
+
|
|
4
|
+
const router: Router = Router();
|
|
5
|
+
export default router;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router.route("/cache/:name").get(imageOptimiser);
|
|
11
|
+
router.route("/createstatic").get(createStatic);
|
|
12
|
+
router.route('/addcomponent').post(addComponent);
|
|
13
|
+
router.route('/addpage').post(addPage);
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
router.route('/').get((req, res) => res.render('_'));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// server.ts
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import frontendRoute from './routes/gardener.route.js'
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
app.set('views', './src/frontend/views');
|
|
11
|
+
app.set("view engine", "ejs");
|
|
12
|
+
app.use(express.static('./src/frontend'));
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
app.use(frontendRoute);
|
|
15
|
+
|
|
16
|
+
const PORT = 5000;
|
|
17
|
+
//
|
|
18
|
+
// initDB().then(
|
|
19
|
+
// () => {
|
|
20
|
+
// app.listen(PORT, () => {
|
|
21
|
+
// console.log("server listening 🚀🚀🚀 PORT:", PORT);
|
|
22
|
+
// });
|
|
23
|
+
// }
|
|
24
|
+
// )
|
|
25
|
+
|
|
26
|
+
app.listen(PORT, () => {
|
|
27
|
+
console.log("server listening 🚀🚀🚀 PORT:", PORT);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
Binary file
|