create-gardener 1.1.11 → 1.1.13
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 +353 -21
- package/package.json +1 -1
- package/template/src/backend/controllers/gardener.controller.ts +7 -1
- package/template/src/backend/routes/gardener.route.ts +4 -3
- package/template/src/frontend/gardenerST.js +21 -20
- package/template/src/frontend/static/gardener.js +16 -21
- package/template/src/frontend/static/global.js +1 -0
package/Readme.md
CHANGED
|
@@ -1,43 +1,375 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
# 🌱 Gardener
|
|
4
|
+
|
|
5
|
+
**Gardener** is a lightweight, DOM-first front-end library for building and manipulating HTML/SVG elements using a clean, declarative JavaScript object syntax.
|
|
6
|
+
|
|
7
|
+
No virtual DOM.
|
|
8
|
+
No JSX.
|
|
9
|
+
No compilation step.
|
|
10
|
+
No magic.
|
|
11
|
+
|
|
12
|
+
Everything is explicit and inspectable directly in the browser.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Philosophy
|
|
17
|
+
|
|
18
|
+
Gardener follows a **DOM-first, deterministic approach**:
|
|
19
|
+
|
|
20
|
+
* No Virtual DOM
|
|
21
|
+
* No JSX
|
|
22
|
+
* No compilation / bundler required
|
|
23
|
+
* No runtime abstraction layers
|
|
24
|
+
* Everything renders directly to the real DOM
|
|
25
|
+
* Works natively in the browser (ES modules)
|
|
26
|
+
|
|
27
|
+
If you can inspect it in DevTools, you can understand it.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Features
|
|
32
|
+
|
|
33
|
+
* Declarative object-based DOM creation
|
|
34
|
+
* Automatic SVG namespace handling
|
|
35
|
+
* Development server support
|
|
36
|
+
* Hot Reload (dev mode)
|
|
37
|
+
* HTML → Component parser
|
|
38
|
+
* Dynamic image resizing & caching
|
|
39
|
+
* Static site generation
|
|
40
|
+
* Lightweight reactive `State` system
|
|
41
|
+
* Zero build step required
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
# 📦 Installation
|
|
46
|
+
|
|
47
|
+
Simply include the module:
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script type="module" src="/gardener.js"></script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
No bundler required.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# 🌿 Core API
|
|
58
|
+
|
|
59
|
+
## 1️⃣ `gardener()` — Declarative Element Builder
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
gardener({
|
|
63
|
+
t: 'div',
|
|
64
|
+
cn: ['p-6', 'flex', 'gap-4'],
|
|
65
|
+
attr: { id: 'hero', role: 'banner' },
|
|
66
|
+
txt: 'Welcome',
|
|
67
|
+
events: {
|
|
68
|
+
click: () => console.log('clicked!')
|
|
69
|
+
},
|
|
70
|
+
children: [
|
|
71
|
+
{ t: 'span', txt: 'Nested child' }
|
|
72
|
+
]
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Supported Keys
|
|
77
|
+
|
|
78
|
+
| Key | Description |
|
|
79
|
+
| ---------- | -------------------------------- |
|
|
80
|
+
| `t` | Tag name (HTML or SVG) |
|
|
81
|
+
| `cn` | Array of class names |
|
|
82
|
+
| `attr` | Attributes / properties object |
|
|
83
|
+
| `txt` | Text content |
|
|
84
|
+
| `events` | `{ eventName: handler }` |
|
|
85
|
+
| `children` | Array of nested gardener objects |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 2️⃣ DOM Helpers
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
fetchElement(selector)
|
|
93
|
+
appendElement(parent, child)
|
|
94
|
+
replaceElement(oldElement, newElement)
|
|
95
|
+
createElement(type, classes)
|
|
96
|
+
insertText(element, text)
|
|
97
|
+
addEL(parent, event, handler)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const el = gardener({ t: 'h1', txt: 'Hello' });
|
|
104
|
+
appendElement('body', el);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
# 🔄 SVG Support
|
|
110
|
+
|
|
111
|
+
Gardener automatically uses the correct SVG namespace for:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
svg, path, circle, rect, line, polygon, polyline, g, defs, clipPath, use
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
gardener({
|
|
121
|
+
t: 'svg',
|
|
122
|
+
attr: { width: 100, height: 100 },
|
|
123
|
+
children: [
|
|
124
|
+
{ t: 'circle', attr: { cx: 50, cy: 50, r: 40, fill: 'red' } }
|
|
125
|
+
]
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
# 🧠 State Management
|
|
132
|
+
|
|
133
|
+
A minimal reactive system.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const count = new State(0);
|
|
137
|
+
|
|
138
|
+
count.registerCb(value => {
|
|
139
|
+
console.log("New value:", value);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
count.setTo(1);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### API
|
|
146
|
+
|
|
147
|
+
* `new State(initialValue)`
|
|
148
|
+
* `.registerCb(callback)`
|
|
149
|
+
* `.setTo(newValue)`
|
|
150
|
+
|
|
151
|
+
No proxies. No diffing. Just callbacks.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
# 🔥 Development Mode
|
|
156
|
+
|
|
157
|
+
Configure runtime behavior:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const config = {
|
|
161
|
+
mode: 'dev', // 'dev' | 'prod'
|
|
162
|
+
componentdir: 'static/components',
|
|
163
|
+
hotreload: false
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Dev Mode Includes:
|
|
168
|
+
|
|
169
|
+
* Floating "+" page creator
|
|
170
|
+
* Component parser
|
|
171
|
+
* Hot reload toggle
|
|
172
|
+
* Static site builder
|
|
2
173
|
|
|
3
|
-
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
# ♻️ Hot Reload
|
|
177
|
+
|
|
178
|
+
* State stored in `localStorage`
|
|
179
|
+
* Reloads page ~1 second after change
|
|
180
|
+
* Toggle with:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Alt + H
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Or use the built-in checkbox (dev mode only).
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
# 🧩 Component Parser
|
|
191
|
+
|
|
192
|
+
Turn existing HTML into reusable JS components.
|
|
193
|
+
|
|
194
|
+
### Step 1 — Write HTML
|
|
195
|
+
|
|
196
|
+
```html
|
|
197
|
+
<div id="user-card">
|
|
198
|
+
Hello, ?name?
|
|
199
|
+
</div>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Step 2 — Run in console
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
parser('#user-card')
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Step 3 — Generated File
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
export default function thisfun({ name }) {
|
|
212
|
+
return gardener({
|
|
213
|
+
t: "div",
|
|
214
|
+
txt: "Hello, " + name
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 🧾 Parameter Syntax
|
|
222
|
+
|
|
223
|
+
Use:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
?variable?
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Gardener extracts variables and generates a function with those parameters.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
# 🖼 Image Optimization
|
|
234
|
+
|
|
235
|
+
Place originals in:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
/src/frontend/assets/
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use:
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<img src="/static/cache/photo_800x600.webp">
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Server will:
|
|
4
248
|
|
|
5
|
-
|
|
249
|
+
* Resize
|
|
250
|
+
* Convert to WebP
|
|
251
|
+
* Cache automatically
|
|
6
252
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* a deterministic static output for production
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
# 📄 Create New Pages (Dev Mode)
|
|
11
256
|
|
|
12
|
-
|
|
257
|
+
1. Click floating `+`
|
|
258
|
+
2. Enter route (e.g., `/about`)
|
|
259
|
+
3. Page auto-generated
|
|
260
|
+
4. Browser redirects
|
|
13
261
|
|
|
14
|
-
|
|
262
|
+
Template used:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
/src/backend/frontendtemplate.ejs
|
|
266
|
+
```
|
|
15
267
|
|
|
16
|
-
`npm create-gardener filename`
|
|
17
268
|
---
|
|
18
269
|
|
|
19
|
-
|
|
270
|
+
# 🏗 Static Site Generation
|
|
20
271
|
|
|
272
|
+
Visit:
|
|
21
273
|
|
|
22
|
-
|
|
274
|
+
```
|
|
275
|
+
/createstatic
|
|
276
|
+
```
|
|
23
277
|
|
|
278
|
+
Build output:
|
|
24
279
|
|
|
280
|
+
```
|
|
281
|
+
/src/frontendStatic
|
|
282
|
+
```
|
|
25
283
|
|
|
284
|
+
---
|
|
26
285
|
|
|
27
|
-
|
|
286
|
+
# 🧩 How It Works
|
|
28
287
|
|
|
29
|
-
|
|
288
|
+
Gardener:
|
|
30
289
|
|
|
31
|
-
|
|
290
|
+
1. Converts object → real DOM
|
|
291
|
+
2. Applies attributes safely
|
|
292
|
+
3. Handles properties vs attributes
|
|
293
|
+
4. Recursively renders children
|
|
294
|
+
5. Avoids virtual DOM entirely
|
|
32
295
|
|
|
33
|
-
|
|
34
|
-
* documentation improvements
|
|
35
|
-
* build pipeline enhancements
|
|
36
|
-
* security hardening
|
|
296
|
+
The browser is the rendering engine.
|
|
37
297
|
|
|
38
298
|
---
|
|
39
299
|
|
|
40
|
-
|
|
300
|
+
# 🎯 When To Use Gardener
|
|
301
|
+
|
|
302
|
+
Good for:
|
|
303
|
+
|
|
304
|
+
* Lightweight SPAs
|
|
305
|
+
* Internal tools
|
|
306
|
+
* Static + hybrid sites
|
|
307
|
+
* Dev tooling
|
|
308
|
+
* Projects where you want full DOM control
|
|
309
|
+
* No-build-step environments
|
|
310
|
+
|
|
311
|
+
Not designed to replace React/Vue — designed to avoid them when unnecessary.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
# 🛠 Example App
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
import { gardener, appendElement } from './gardener.js';
|
|
319
|
+
|
|
320
|
+
const app = gardener({
|
|
321
|
+
t: 'div',
|
|
322
|
+
cn: ['app'],
|
|
323
|
+
children: [
|
|
324
|
+
{ t: 'h1', txt: 'Hello World' },
|
|
325
|
+
{
|
|
326
|
+
t: 'button',
|
|
327
|
+
txt: 'Click Me',
|
|
328
|
+
events: {
|
|
329
|
+
click: () => alert('Hi!')
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
appendElement('body', app);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
# 📁 Project Structure (Suggested)
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
/src
|
|
344
|
+
/frontend
|
|
345
|
+
/backend
|
|
346
|
+
/frontendStatic
|
|
347
|
+
/static
|
|
348
|
+
/components
|
|
349
|
+
gardener.js
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
# 🧭 Roadmap Ideas
|
|
355
|
+
|
|
356
|
+
* Fine-grained reactive bindings
|
|
357
|
+
* Dev inspector panel
|
|
358
|
+
* Server-side rendering mode
|
|
359
|
+
* Plugin API
|
|
360
|
+
* Optional diff mode
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
# 📜 License
|
|
365
|
+
|
|
366
|
+
MIT (recommended)
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
# 🌿 Why Gardener?
|
|
371
|
+
|
|
372
|
+
Because sometimes you don't need a forest.
|
|
41
373
|
|
|
42
|
-
|
|
374
|
+
Just a garden.
|
|
43
375
|
|
package/package.json
CHANGED
|
@@ -16,10 +16,12 @@ interface AddComponentBody {
|
|
|
16
16
|
component: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function addComponent(req: Request<{}, {}, AddComponentBody>, res: Response) {
|
|
19
|
+
export async function addComponent(req: Request<{}, {}, AddComponentBody>, res: Response) {
|
|
20
20
|
try {
|
|
21
21
|
const { path: filePath, component } = req.body;
|
|
22
22
|
|
|
23
|
+
await fsp.mkdir('./src/frontend/static/components', { recursive: true });
|
|
24
|
+
|
|
23
25
|
fs.writeFileSync(`./src/frontend/${filePath}`, component, "utf8");
|
|
24
26
|
|
|
25
27
|
res.json({ success: true });
|
|
@@ -101,6 +103,9 @@ export async function addPage(req: Request, res: Response) {
|
|
|
101
103
|
|
|
102
104
|
fs.appendFileSync('./src/backend/routes/gardener.route.ts', ` router.route("${pagename}").get((req: Request, res: Response) => res.render("${name}"))\n `);
|
|
103
105
|
|
|
106
|
+
await fsp.mkdir('src/frontend/static/pages', { recursive: true });
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(`./src/frontend/static/pages/${name}.js`, 'import { log, parser, fetchElement, replaceElement, appendElement, State, addEL } from "/static/gardener.js";', "utf8");
|
|
104
109
|
res.json({ success: true });
|
|
105
110
|
}
|
|
106
111
|
catch (err) {
|
|
@@ -185,3 +190,4 @@ export async function createStatic(req: Request, res: Response) {
|
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import { Router } from "express";
|
|
2
3
|
import { addComponent, addPage, createStatic, imageOptimiser } from "../controllers/gardener.controller.js";
|
|
3
4
|
|
|
4
5
|
const router: Router = Router();
|
|
@@ -16,6 +17,6 @@ router.route('/addpage').post(addPage);
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
router.route('/').get((req, res) => res.render('_'));
|
|
20
|
-
router.route('/login').get((req, res) => res.render('_login'));
|
|
20
|
+
router.route('/').get((req: Request, res: Response) => res.render('_'));
|
|
21
|
+
router.route('/login').get((req: Request, res: Response) => res.render('_login'));
|
|
21
22
|
|
|
@@ -323,6 +323,14 @@ function generateFile(obj) {
|
|
|
323
323
|
const formatted = JSON.stringify(obj, null, 2);
|
|
324
324
|
const { cleanedString, extractedList } = cleanStringAndList(formatted);
|
|
325
325
|
|
|
326
|
+
if (extractedList.length === 0) return `
|
|
327
|
+
import { gardener, fetchElement, replaceElement } from '../gardener.js'
|
|
328
|
+
|
|
329
|
+
export default function thisfun() {
|
|
330
|
+
return gardener(${cleanedString})
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
|
|
326
334
|
return `
|
|
327
335
|
import { gardener, fetchElement, replaceElement } from '../gardener.js'
|
|
328
336
|
|
|
@@ -394,7 +402,6 @@ export function parser(element, isParent = true) {
|
|
|
394
402
|
return obj
|
|
395
403
|
|
|
396
404
|
|
|
397
|
-
//Let Browser do the migration from html to json and then use copy paste
|
|
398
405
|
}
|
|
399
406
|
|
|
400
407
|
|
|
@@ -405,26 +412,20 @@ export function addEL(parent, event, fun) {
|
|
|
405
412
|
parent.addEventListener(event, fun)
|
|
406
413
|
}
|
|
407
414
|
|
|
408
|
-
export function imagePreloader(images) {
|
|
409
|
-
const body = fetchElement('body')
|
|
410
|
-
images.forEach(entry => {
|
|
411
|
-
appendElement(body, gardener({
|
|
412
|
-
t: 'img',
|
|
413
|
-
cn: ['preloaderimage'],
|
|
414
|
-
attr: {
|
|
415
|
-
src: entry,
|
|
416
|
-
alt: entry
|
|
417
|
-
}
|
|
418
|
-
}));
|
|
419
415
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
416
|
+
export class State {
|
|
417
|
+
constructor(value) {
|
|
418
|
+
this.value = value;
|
|
419
|
+
this.cb = [];
|
|
420
|
+
}
|
|
421
|
+
registerCb(cb) {
|
|
422
|
+
cb(this.value);
|
|
423
|
+
this.cb.push(cb);
|
|
424
|
+
}
|
|
425
|
+
setTo(val) {
|
|
426
|
+
this.value = val;
|
|
427
|
+
this.cb.forEach(cb => { cb(val) });
|
|
428
|
+
}
|
|
426
429
|
}
|
|
427
430
|
|
|
428
431
|
|
|
429
|
-
|
|
430
|
-
|
|
@@ -383,9 +383,6 @@ export default function thisfun({${extractedList}}) {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
|
|
386
|
-
// Example:
|
|
387
|
-
// const result = cleanStringAndList('hi "{ritish}" how are you');
|
|
388
|
-
// console.log(result.cleanedString); // "hi ritish how are you"
|
|
389
386
|
// console.log(result.extractedList); // ["ritish"]
|
|
390
387
|
|
|
391
388
|
|
|
@@ -458,25 +455,23 @@ export function addEL(parent, event, fun) {
|
|
|
458
455
|
parent.addEventListener(event, fun)
|
|
459
456
|
}
|
|
460
457
|
|
|
461
|
-
export
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const images = document.querySelectorAll('.preloaderimage');
|
|
475
|
-
images.forEach(entry => { entry.style.display = 'none' });
|
|
476
|
-
}, 0)
|
|
477
|
-
|
|
478
|
-
})
|
|
458
|
+
export class State {
|
|
459
|
+
constructor(value) {
|
|
460
|
+
this.value = value;
|
|
461
|
+
this.cb = [];
|
|
462
|
+
}
|
|
463
|
+
registerCb(cb) {
|
|
464
|
+
cb(this.value);
|
|
465
|
+
this.cb.push(cb);
|
|
466
|
+
}
|
|
467
|
+
setTo(val) {
|
|
468
|
+
this.value = val;
|
|
469
|
+
this.cb.forEach(cb => { cb(val) });
|
|
470
|
+
}
|
|
479
471
|
}
|
|
480
472
|
|
|
481
473
|
|
|
474
|
+
export function log(target) {
|
|
475
|
+
console.log(target)
|
|
476
|
+
}
|
|
482
477
|
|