create-gardener 1.1.6 → 1.1.7

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/template/src/backend/controllers/gardener.controller.ts +6 -19
  3. package/template/src/backend/routes/gardener.route.ts +2 -1
  4. package/template/src/backend/server.ts +1 -1
  5. package/template/src/frontend/frontendtemplate.ejs +4 -4
  6. package/template/src/frontend/gardenerST.js +340 -0
  7. package/template/src/frontend/static/components/emailsvg.js +55 -0
  8. package/template/src/frontend/{components → static/components}/eyeoff.js +1 -1
  9. package/template/src/frontend/static/components/eyeon.js +43 -0
  10. package/template/src/frontend/{components → static/components}/notification.js +1 -1
  11. package/template/src/frontend/{components → static/components}/passwordBox.js +3 -3
  12. package/template/src/frontend/static/components/test.js +54 -0
  13. package/template/src/frontend/{gardener.js → static/gardener.js} +14 -1
  14. package/template/src/frontend/{style.css → static/style.css} +148 -347
  15. package/template/src/frontend/views/_.ejs +8 -8
  16. package/template/src/frontend/views/_login.ejs +8 -8
  17. package/.direnv/bin/nix-direnv-reload +0 -19
  18. package/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc +0 -2140
  19. package/.envrc +0 -1
  20. package/template/src/frontend/components/emailsvg.js +0 -55
  21. package/template/src/frontend/components/eyeon.js +0 -44
  22. package/template/src/frontend/components/test.js +0 -54
  23. /package/template/src/{backend → frontend/static}/cache/gardener_500x500.webp +0 -0
  24. /package/template/src/frontend/{components → static/components}/nonui/api.js +0 -0
  25. /package/template/src/frontend/{global.js → static/global.js} +0 -0
  26. /package/template/src/frontend/{style2.css → static/style2.css} +0 -0
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "https://github.com/ritishDas/gardener"
7
7
  },
8
- "version": "1.1.6",
8
+ "version": "1.1.7",
9
9
  "description": "A dom gardener converting dom elements into json and vice versa",
10
10
  "main": "index.js",
11
11
  "bin": {
@@ -46,7 +46,7 @@ export async function imageOptimiser(req: Request, res: Response) {
46
46
  try {
47
47
  const { name } = req.params;
48
48
 
49
- if (!name) return;
49
+ if (typeof name !== 'string') return;
50
50
  // name format: test_500x300.webp
51
51
  const match = name.match(/^(.+?)_(\d+)x(\d+)\.webp$/);
52
52
 
@@ -60,7 +60,7 @@ export async function imageOptimiser(req: Request, res: Response) {
60
60
  const width = parseInt(widthStr, 10);
61
61
  const height = parseInt(heightStr, 10);
62
62
 
63
- const cacheDir = path.join(__dirname, "../cache");
63
+ const cacheDir = path.join(__dirname, "../../frontend/static/cache");
64
64
  await fsp.mkdir(cacheDir, { recursive: true });
65
65
 
66
66
  const outputPath = path.join(cacheDir, name);
@@ -129,22 +129,13 @@ export async function createStatic(req: Request, res: Response) {
129
129
  const outDir = path.resolve("src/tempfrontend");
130
130
  const finalOut = path.resolve("src/frontendStatic");
131
131
 
132
- const otherAssets = path.resolve("src/frontend");
133
132
  await fsp.mkdir(outDir, { recursive: true });
133
+ await fsp.mkdir(finalOut, { recursive: true });
134
134
 
135
- const entries2 = await fsp.readdir(otherAssets, { withFileTypes: true });
136
135
  const entries = await fsp.readdir(viewsDir, { withFileTypes: true });
137
136
 
138
137
  const rendered: string[] = [];
139
138
 
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
139
 
149
140
  for (const entry of entries) {
150
141
  // skip folders (partials, layouts, etc.)
@@ -190,15 +181,11 @@ export async function createStatic(req: Request, res: Response) {
190
181
  }
191
182
  await fsp.rm(outDir, { recursive: true, force: true });
192
183
  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'),
184
+ path.resolve("src/frontend/static"),
185
+ path.join(finalOut, 'static'),
200
186
  { recursive: true }
201
187
  );
188
+
202
189
  return res.json({
203
190
  success: true,
204
191
  generated: rendered,
@@ -7,7 +7,7 @@ export default router;
7
7
 
8
8
 
9
9
 
10
- router.route("/cache/:name").get(imageOptimiser);
10
+ router.route("/static/cache/:name").get(imageOptimiser);
11
11
  router.route("/createstatic").get(createStatic);
12
12
  router.route('/addcomponent').post(addComponent);
13
13
  router.route('/addpage').post(addPage);
@@ -18,3 +18,4 @@ router.route('/addpage').post(addPage);
18
18
 
19
19
  router.route('/').get((req, res) => res.render('_'));
20
20
  router.route('/login').get((req, res) => res.render('_login'));
21
+
@@ -13,7 +13,7 @@ app.use(express.static('./src/frontend'));
13
13
  app.use(express.json());
14
14
  app.use(frontendRoute);
15
15
 
16
- const PORT = 5000;
16
+ const PORT = process.env.PORT || 3000;
17
17
  //
18
18
  // initDB().then(
19
19
  // () => {
@@ -3,8 +3,8 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <link rel="stylesheet" href="/style.css">
7
- <link rel="stylesheet" href="/style2.css">
6
+ <link href="/static/style.css" rel="stylesheet"/>
7
+ <link href="/static/style2.css" rel="stylesheet"/>
8
8
  <title>Redvent:Login</title>
9
9
  </head>
10
10
  <body>
@@ -24,10 +24,10 @@
24
24
  <img src="/cache/w_500x500.webp" alt="logo" class="w-500">
25
25
  </div>
26
26
  </div>
27
- <script src='/global.js' type="module"></script>
27
+ <script src='/static/global.js' type="module"></script>
28
28
  <script type="module">
29
29
 
30
- import { parser, fetchElement, replaceElement, appendElement } from "/gardener.js";
30
+ import { parser, fetchElement, replaceElement, appendElement } from "/static/gardener.js";
31
31
  </script>
32
32
  </body>
33
33
  </html>
@@ -0,0 +1,340 @@
1
+ //Standalone gardener version for no backend environment
2
+
3
+
4
+
5
+ const config = {
6
+ mode: 'dev',
7
+ hotreload: false
8
+ }
9
+
10
+ let hotReloadtimeout;
11
+ const body = fetchElement('body');
12
+ let hotReload = localStorage.getItem('hotreload');
13
+
14
+ if (hotReload === null) hotReload = config.hotreload;
15
+ else if (hotReload === 'true') hotReload = true
16
+ else if (hotReload === 'false') hotReload = false
17
+
18
+
19
+
20
+
21
+ appendElement(body, gardener({
22
+ t: 'p',
23
+ cn: ['bg-gray-200', 'fixed', 'bottom-0', 'z-100', 'right-0', 'border-b-1', 'p-2', 'rounded-md'],
24
+ children: [
25
+ {
26
+ t: 'span',
27
+ txt: 'Press '
28
+ },
29
+ {
30
+ t: 'span',
31
+ cn: ['text-green-500', 'font-bold'],
32
+ txt: 'Ctrl+h'
33
+ },
34
+ {
35
+ t: 'span',
36
+ txt: ' to toggle Hot Reload'
37
+ },
38
+ {
39
+ t: 'form',
40
+ attr: {
41
+ id: 'hrcheckbox',
42
+ },
43
+ events: {
44
+ click: () => togglehotreload()
45
+ },
46
+ cn: ['p-2', 'bg-red-300'],
47
+ children: [{
48
+ t: 'label',
49
+ txt: 'Hot Reload ',
50
+ }
51
+ , {
52
+ t: 'input',
53
+ cn: ['hrcheckbox'],
54
+ attr: {
55
+ type: 'checkbox'
56
+ }
57
+ }]
58
+ }
59
+ ]
60
+ }))
61
+
62
+ //appendElement(body, gardener())
63
+
64
+
65
+ function applyHotReloadState() {
66
+ const hrcheck = fetchElement('#hrcheckbox');
67
+ const checkbox = fetchElement('.hrcheckbox');
68
+
69
+ if (hotReload) {
70
+ hrcheck.style.background = '#66e666';
71
+ checkbox.checked = true;
72
+ hotReloadtimeout = setTimeout(() => window.location.reload(), 1000);
73
+ } else {
74
+ hrcheck.style.background = 'red';
75
+ checkbox.checked = false;
76
+ clearTimeout(hotReloadtimeout);
77
+ }
78
+ }
79
+
80
+ applyHotReloadState();
81
+
82
+ // togglehotreload();
83
+
84
+
85
+
86
+ document.addEventListener('keydown', function(e) {
87
+ // Detect Ctrl + H
88
+ if (e.ctrlKey && e.key.toLowerCase() === 'h') {
89
+ e.preventDefault(); // Stop browser from opening history
90
+ // Your logic here...
91
+ togglehotreload();
92
+ }
93
+ });
94
+
95
+ //if (config.mode === 'dev') {
96
+
97
+
98
+ function togglehotreload() {
99
+ hotReload = !hotReload;
100
+ localStorage.setItem('hotreload', String(hotReload));
101
+
102
+ const hrcheck = fetchElement('#hrcheckbox');
103
+ const checkbox = fetchElement('.hrcheckbox');
104
+
105
+ if (hotReload) {
106
+ hrcheck.style.background = '#66e666';
107
+ checkbox.checked = true;
108
+ hotReloadtimeout = setTimeout(() => window.location.reload(), 1000);
109
+ } else {
110
+ hrcheck.style.background = 'red';
111
+ checkbox.checked = false;
112
+ clearTimeout(hotReloadtimeout);
113
+ }
114
+ }
115
+
116
+ export function parserWindow(input) {
117
+
118
+
119
+ const parsed = JSON.parse(input);
120
+ const text = JSON.stringify(parsed, null, 1);
121
+ if (config.mode !== 'dev') return;
122
+
123
+
124
+ const result = gardener({
125
+ t: 'div',
126
+ cn: ['fixed', 'border-2', 'border-black', 'bg-gray-500', 'text-white', 'rounded-lg', 'z-90', 'w-2/4', 'h-2/4', 'left-1/4', 'flex', 'flex-col', 'justify-between', 'top-1/4'],
127
+ children: [
128
+ {
129
+ t: 'div',
130
+ cn: ['bg-gray-200', 'h-15', 'text-black', 'rounded-t-lg', 'flex', 'items-center', 'justify-around'],
131
+ children: [
132
+ {
133
+ t: 'h3',
134
+ cn: ['font-bold'],
135
+ txt: 'Parser Window'
136
+ },
137
+ {
138
+ t: 'button',
139
+ cn: ['p-2', 'bg-red-300', 'rounded-lg', 'cursor-pointer'],
140
+ txt: 'Add Component',
141
+ attr: {
142
+ id: 'copybtn'
143
+ },
144
+ events: {
145
+ click: (e) => { copyTextToClipboard(text) }
146
+ }
147
+ }
148
+ ]
149
+ },
150
+ {
151
+ t: 'pre',
152
+ cn: ['p-5', 'overflow-scroll', 'text-sm'],
153
+ txt: text
154
+ }
155
+ ]
156
+ })
157
+
158
+
159
+ appendElement(body, result);
160
+ }
161
+
162
+
163
+
164
+ async function copyTextToClipboard(txt) {
165
+ try {
166
+ await navigator.clipboard.writeText(txt);
167
+ console.log('Component copied to clipboard');
168
+ } catch (err) {
169
+ console.error('Clipboard copy failed', err);
170
+ }
171
+ }
172
+
173
+
174
+
175
+
176
+ export function fetchElement(param) {
177
+ return document.querySelector(param);
178
+ }
179
+
180
+ export function appendElement(parent, child) {
181
+ if (typeof parent === 'string') {
182
+ parent = fetchElement(parent);
183
+ }
184
+ parent.appendChild(child);
185
+ }
186
+
187
+ export function createElement(type, classname) {
188
+ let element = document.createElement(type);
189
+ if (classname)
190
+ element.classList.add(...classname);
191
+ return element;
192
+ }
193
+
194
+ export function insertText(element, text) {
195
+ element.innerText = text;
196
+ }
197
+
198
+ export function replaceElement(original, New) {
199
+ if (typeof original === 'string') {
200
+ original = fetchElement(original);
201
+ }
202
+ original.replaceWith(New);
203
+ }
204
+
205
+ export function gardener(Dom) {
206
+
207
+ if (Dom.nodeType === 1) return Dom;
208
+ // detect if this is an SVG element
209
+ const isSVG = [
210
+ 'svg', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'g', 'defs', 'clipPath', 'use'
211
+ ].includes(Dom.t);
212
+
213
+ // create element accordingly
214
+ let element;
215
+
216
+ if (isSVG) {
217
+ element = document.createElementNS('http://www.w3.org/2000/svg', Dom.t);
218
+ if (Dom.cn)
219
+ element.classList.add(...Dom.cn);
220
+ }
221
+ else {
222
+ element = createElement(Dom.t, Dom.cn);
223
+ }
224
+
225
+ // text content (skip for SVG like <path>)
226
+ if (Dom.txt) {
227
+ insertText(element, Dom.txt);
228
+ }
229
+
230
+ // apply attributes safely
231
+ const propertyNames = new Set([
232
+ 'value', 'selected', 'muted', 'disabled',
233
+ 'selectedIndex', 'volume', // etc.
234
+ ]);
235
+
236
+ if (Dom.attr) {
237
+ for (const [key, value] of Object.entries(Dom.attr)) {
238
+ if (isSVG || key.startsWith('data-') || key.startsWith('aria-')) {
239
+ element.setAttribute(key, value);
240
+ } else if (key in element && !propertyNames.has(key)) {
241
+ // Prefer property for known safe cases
242
+ try { element[key] = value === '' ? true : value; } catch (e) { element.setAttribute(key, value); }
243
+ } else {
244
+ element.setAttribute(key, value);
245
+ }
246
+ }
247
+ }
248
+
249
+ if (Dom.events) {
250
+ Object.entries(Dom.events).forEach(([eventName, handler]) => {
251
+ element.addEventListener(eventName, handler);
252
+ });
253
+ }
254
+
255
+ // recursively handle children
256
+ if (Dom.children) {
257
+ Dom.children.forEach(child => appendElement(element, gardener(child)));
258
+ }
259
+
260
+ return element;
261
+ }
262
+
263
+ export function parser(element, isParent = true) {
264
+ if (typeof element === 'string') {
265
+ element = fetchElement(element);
266
+ // If user passes raw HTML string
267
+ }
268
+
269
+ const obj = {
270
+ t: element.tagName.toLowerCase(),
271
+ };
272
+
273
+ // add classes if present
274
+ if (element.classList.length) {
275
+ obj.cn = Array.from(element.classList);
276
+ }
277
+
278
+ // add attributes if present
279
+ const attrs = {};
280
+ for (const attr of element.attributes) {
281
+ if (attr.name !== 'class') attrs[attr.name] = attr.value;
282
+ }
283
+ if (Object.keys(attrs).length) obj.attr = attrs;
284
+ // add text content (only if no children)
285
+ if (element.childNodes.length === 1 && element.firstChild.nodeType === Node.TEXT_NODE) {
286
+ obj.txt = element.textContent.trim();
287
+
288
+ if (isParent) {
289
+ parserWindow(JSON.stringify(obj))
290
+ }
291
+
292
+ return obj;
293
+ }
294
+
295
+ // add children recursively
296
+ const children = [];
297
+ for (const child of element.children) {
298
+ children.push(parser(child, false));
299
+ }
300
+ if (children.length) obj.children = children;
301
+
302
+
303
+ if (isParent) {
304
+ parserWindow(JSON.stringify(obj))
305
+ }
306
+
307
+ return obj
308
+ //Let Browser do the migration from html to json and then use copy paste
309
+ }
310
+
311
+ export function addEL(parent, event, fun) {
312
+ if (typeof parent === 'string') {
313
+ parent = fetchElement(parent);
314
+ }
315
+ parent.addEventListener(event, fun)
316
+ }
317
+
318
+
319
+
320
+ export function imagePreloader(images) {
321
+ const body = fetchElement('body')
322
+ images.forEach(entry => {
323
+ appendElement(body, gardener({
324
+ t: 'img',
325
+ cn: ['preloaderimage'],
326
+ attr: {
327
+ src: entry,
328
+ alt: entry
329
+ }
330
+ }));
331
+
332
+ setTimeout(() => {
333
+ const images = document.querySelectorAll('.preloaderimage');
334
+ images.forEach(entry => { entry.style.display = 'none' });
335
+ }, 0)
336
+
337
+ })
338
+ }
339
+
340
+
@@ -0,0 +1,55 @@
1
+
2
+ import { gardener, fetchElement, replaceElement } from '/static/gardener.js'
3
+
4
+ export default function thisfun() {
5
+ return gardener({
6
+ "t": "span",
7
+ "cn": [
8
+ "emailsvg"
9
+ ],
10
+ "children": [
11
+ {
12
+ "t": "svg",
13
+ "cn": [
14
+ "icon",
15
+ "icon-tabler",
16
+ "icons-tabler-outline",
17
+ "icon-tabler-mail"
18
+ ],
19
+ "attr": {
20
+ "xmlns": "http://www.w3.org/2000/svg",
21
+ "width": "24",
22
+ "height": "24",
23
+ "viewBox": "0 0 24 24",
24
+ "fill": "none",
25
+ "stroke": "currentColor",
26
+ "stroke-width": "2",
27
+ "stroke-linecap": "round",
28
+ "stroke-linejoin": "round"
29
+ },
30
+ "children": [
31
+ {
32
+ "t": "path",
33
+ "attr": {
34
+ "stroke": "none",
35
+ "d": "M0 0h24v24H0z",
36
+ "fill": "none"
37
+ }
38
+ },
39
+ {
40
+ "t": "path",
41
+ "attr": {
42
+ "d": "M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10"
43
+ }
44
+ },
45
+ {
46
+ "t": "path",
47
+ "attr": {
48
+ "d": "M3 7l9 6l9 -6"
49
+ }
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ })
55
+ }
@@ -1,5 +1,5 @@
1
1
 
2
- import { gardener } from '../gardener.js'
2
+ import { gardener } from '/static/gardener.js'
3
3
 
4
4
  export default function () {
5
5
  return gardener({
@@ -0,0 +1,43 @@
1
+
2
+ import { gardener } from '/static/gardener.js'
3
+ export default function() {
4
+ return gardener({
5
+ "t": "svg",
6
+ "cn": [
7
+ "eye"
8
+ ],
9
+ "attr": {
10
+ "xmlns": "http://www.w3.org/2000/svg",
11
+ "width": "24",
12
+ "height": "24",
13
+ "viewBox": "0 0 24 24",
14
+ "fill": "none",
15
+ "stroke": "currentColor",
16
+ "stroke-width": "2",
17
+ "stroke-linecap": "round",
18
+ "stroke-linejoin": "round"
19
+ },
20
+ "children": [
21
+ {
22
+ "t": "path",
23
+ "attr": {
24
+ "stroke": "none",
25
+ "d": "M0 0h24v24H0z",
26
+ "fill": "none"
27
+ }
28
+ },
29
+ {
30
+ "t": "path",
31
+ "attr": {
32
+ "d": "M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"
33
+ }
34
+ },
35
+ {
36
+ "t": "path",
37
+ "attr": {
38
+ "d": "M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"
39
+ }
40
+ }
41
+ ]
42
+ })
43
+ }
@@ -1,4 +1,4 @@
1
- import { gardener, replaceElement, fetchElement } from '/gardener.js';
1
+ import { gardener, replaceElement, fetchElement } from '/static/gardener.js';
2
2
 
3
3
  export default function addNotification(noti) {
4
4
  console.log('clicked');
@@ -1,6 +1,6 @@
1
- import { gardener, replaceElement, fetchElement } from '/gardener.js'
2
- import eyeoff from '/components/eyeoff.js'
3
- import eyeon from '/components/eyeon.js'
1
+ import { gardener, replaceElement, fetchElement } from '/static/gardener.js'
2
+ import eyeoff from '/static/components/eyeoff.js'
3
+ import eyeon from '/static/components/eyeon.js'
4
4
  export default function thisfun(visible) {
5
5
 
6
6
  const svg = visible ? eyeoff() : eyeon();
@@ -0,0 +1,54 @@
1
+
2
+ import { gardener, fetchElement, replaceElement } from '/static/gardener.js'
3
+
4
+ export default function thisfun() {
5
+ return gardener({
6
+ "t": "div",
7
+ "attr": {
8
+ "id": "body"
9
+ },
10
+ "children": [
11
+ {
12
+ "t": "div",
13
+ "cn": [
14
+ "h-screen",
15
+ "w-screen",
16
+ "bg-white",
17
+ "loader",
18
+ "absolute"
19
+ ],
20
+ "attr": {
21
+ "style": "transition: 0.4s; opacity: 0;"
22
+ },
23
+ "txt": ""
24
+ },
25
+ {
26
+ "t": "div",
27
+ "cn": [
28
+ "hero",
29
+ "flex",
30
+ "justify-around",
31
+ "items-center",
32
+ "p-5",
33
+ "h-[90vh]"
34
+ ],
35
+ "children": [
36
+ {
37
+ "t": "p",
38
+ "cn": [
39
+ "p-5"
40
+ ],
41
+ "txt": "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 timely reload and on-the-fly component creation from existing HTML. The server also provides dynamic image resizing and caching."
42
+ },
43
+ {
44
+ "t": "img",
45
+ "attr": {
46
+ "src": "/cache/gardener_500x500.webp",
47
+ "alt": "logo"
48
+ }
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ })
54
+ }
@@ -1,6 +1,6 @@
1
1
  const config = {
2
2
  mode: 'dev',
3
- componentdir: 'components',
3
+ componentdir: 'static/components',
4
4
  hotreload: false
5
5
  }
6
6
 
@@ -255,6 +255,9 @@ export function fetchElement(param) {
255
255
  }
256
256
 
257
257
  export function appendElement(parent, child) {
258
+ if (typeof parent === 'string') {
259
+ parent = fetchElement(parent);
260
+ }
258
261
  parent.appendChild(child);
259
262
  }
260
263
 
@@ -270,6 +273,9 @@ export function insertText(element, text) {
270
273
  }
271
274
 
272
275
  export function replaceElement(original, New) {
276
+ if (typeof original === 'string') {
277
+ original = fetchElement(original);
278
+ }
273
279
  original.replaceWith(New);
274
280
  }
275
281
 
@@ -378,6 +384,13 @@ export function parser(element, isParent = true) {
378
384
  //Let Browser do the migration from html to json and then use copy paste
379
385
  }
380
386
 
387
+ export function addEL(parent, event, fun) {
388
+ if (typeof parent === 'string') {
389
+ parent = fetchElement(parent);
390
+ }
391
+ parent.addEventListener(event, fun)
392
+ }
393
+
381
394
  export function imagePreloader(images) {
382
395
  const body = fetchElement('body')
383
396
  images.forEach(entry => {