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.
@@ -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
+ }
@@ -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
+