@currentjs/gen 0.1.1

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 (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +56 -0
  3. package/README.md +686 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +143 -0
  6. package/dist/commands/commit.d.ts +1 -0
  7. package/dist/commands/commit.js +153 -0
  8. package/dist/commands/createApp.d.ts +1 -0
  9. package/dist/commands/createApp.js +64 -0
  10. package/dist/commands/createModule.d.ts +1 -0
  11. package/dist/commands/createModule.js +121 -0
  12. package/dist/commands/diff.d.ts +1 -0
  13. package/dist/commands/diff.js +164 -0
  14. package/dist/commands/generateAll.d.ts +4 -0
  15. package/dist/commands/generateAll.js +305 -0
  16. package/dist/commands/infer.d.ts +1 -0
  17. package/dist/commands/infer.js +179 -0
  18. package/dist/generators/controllerGenerator.d.ts +20 -0
  19. package/dist/generators/controllerGenerator.js +280 -0
  20. package/dist/generators/domainModelGenerator.d.ts +33 -0
  21. package/dist/generators/domainModelGenerator.js +175 -0
  22. package/dist/generators/serviceGenerator.d.ts +39 -0
  23. package/dist/generators/serviceGenerator.js +379 -0
  24. package/dist/generators/storeGenerator.d.ts +31 -0
  25. package/dist/generators/storeGenerator.js +191 -0
  26. package/dist/generators/templateGenerator.d.ts +11 -0
  27. package/dist/generators/templateGenerator.js +143 -0
  28. package/dist/generators/templates/appTemplates.d.ts +27 -0
  29. package/dist/generators/templates/appTemplates.js +1621 -0
  30. package/dist/generators/templates/controllerTemplates.d.ts +43 -0
  31. package/dist/generators/templates/controllerTemplates.js +82 -0
  32. package/dist/generators/templates/index.d.ts +5 -0
  33. package/dist/generators/templates/index.js +21 -0
  34. package/dist/generators/templates/serviceTemplates.d.ts +15 -0
  35. package/dist/generators/templates/serviceTemplates.js +54 -0
  36. package/dist/generators/templates/storeTemplates.d.ts +9 -0
  37. package/dist/generators/templates/storeTemplates.js +260 -0
  38. package/dist/generators/templates/validationTemplates.d.ts +25 -0
  39. package/dist/generators/templates/validationTemplates.js +66 -0
  40. package/dist/generators/templates/viewTemplates.d.ts +16 -0
  41. package/dist/generators/templates/viewTemplates.js +359 -0
  42. package/dist/generators/validationGenerator.d.ts +24 -0
  43. package/dist/generators/validationGenerator.js +199 -0
  44. package/dist/utils/cliUtils.d.ts +6 -0
  45. package/dist/utils/cliUtils.js +71 -0
  46. package/dist/utils/colors.d.ts +26 -0
  47. package/dist/utils/colors.js +80 -0
  48. package/dist/utils/commitUtils.d.ts +46 -0
  49. package/dist/utils/commitUtils.js +377 -0
  50. package/dist/utils/constants.d.ts +52 -0
  51. package/dist/utils/constants.js +64 -0
  52. package/dist/utils/generationRegistry.d.ts +25 -0
  53. package/dist/utils/generationRegistry.js +192 -0
  54. package/howto.md +556 -0
  55. package/package.json +44 -0
@@ -0,0 +1,1621 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_FILES = exports.DEFAULT_DIRECTORIES = exports.cursorRulesTemplate = exports.translationsTemplate = exports.frontendScriptTemplate = exports.errorTemplate = exports.mainViewTemplate = exports.appTsTemplate = exports.appYamlTemplate = exports.tsconfigTemplate = exports.packageJsonTemplate = void 0;
37
+ // Application-level templates for project scaffolding
38
+ const path = __importStar(require("path"));
39
+ const constants_1 = require("../../utils/constants");
40
+ const packageJsonTemplate = (appName) => JSON.stringify({
41
+ name: appName || 'my-app',
42
+ version: '0.1.0',
43
+ private: true,
44
+ scripts: {
45
+ build: 'npm run clean && tsc -p tsconfig.json && npx path-fixer',
46
+ start: 'node build/app.js',
47
+ clean: 'rm -rf build',
48
+ dev: 'ts-node src/app.ts',
49
+ devstand: 'npm i && npm link @currentjs/router @currentjs/templating @currentjs/provider-mysql && npm run build'
50
+ },
51
+ dependencies: {},
52
+ devDependencies: {
53
+ typescript: '^5.6.3',
54
+ '@types/node': '^22.7.4',
55
+ '@koz1024/path-fixer': '^0.2.1',
56
+ // '@currentjs/router': '^0.1.0',
57
+ // '@currentjs/templating': '^0.1.0'
58
+ },
59
+ type: 'module'
60
+ }, null, 2);
61
+ exports.packageJsonTemplate = packageJsonTemplate;
62
+ exports.tsconfigTemplate = `{
63
+ "compilerOptions": {
64
+ "module": "es2020",
65
+ "target": "es2020",
66
+ "sourceMap": false,
67
+ "experimentalDecorators": true,
68
+ "emitDecoratorMetadata": true,
69
+ "baseUrl": "./src",
70
+ "outDir": "./build",
71
+ "types": ["node"],
72
+ "moduleResolution": "Node",
73
+ "declaration": true,
74
+ "declarationMap": false
75
+ },
76
+ "exclude": [
77
+ "node_modules"
78
+ ],
79
+ "type": "module"
80
+ }`;
81
+ exports.appYamlTemplate = `
82
+ providers:
83
+ mysql: '@currentjs/provider-mysql'
84
+ database: mysql
85
+ modules: []`;
86
+ exports.appTsTemplate = `import { createWebServer } from '@currentjs/router';
87
+ import { createTemplateEngine } from '@currentjs/templating';
88
+ import * as path from 'path';
89
+
90
+ /** DO NOT CHANGE THESE LINES **/
91
+ ${constants_1.GENERATOR_MARKERS.CONTROLLERS_START}
92
+ ${constants_1.GENERATOR_MARKERS.CONTROLLERS_END}
93
+ /** END OF DO NOT CHANGE THESE LINES **/
94
+
95
+ export async function main() {
96
+ await Promise.all(Object.values(providers).map(provider => provider.init()));
97
+ const webDir = path.join(process.cwd(), 'web');
98
+ const renderEngine = createTemplateEngine({ directories: [process.cwd()] });
99
+ const app = createWebServer({ controllers, webDir }, {
100
+ port: 3000,
101
+ renderer: (template, data, layout) => {
102
+ try {
103
+ return layout ? renderEngine.renderWithLayout(layout, template, data) : renderEngine.render(template, data);
104
+ } catch (e) {
105
+ return String(e instanceof Error ? e.message : e);
106
+ }
107
+ },
108
+ errorTemplate: 'error'
109
+ });
110
+ await app.listen();
111
+ console.log('Server started on http://localhost:3000');
112
+
113
+ const handleTermination = async () => {
114
+ try { await app.close(); } catch {}
115
+ const shutdowns = Object.values(providers).map(provider => provider.shutdown ?? (() => {}));
116
+ await Promise.all(shutdowns);
117
+ process.exit(0);
118
+ };
119
+
120
+ process.on('SIGINT', handleTermination);
121
+ process.on('SIGTERM', handleTermination);
122
+ }
123
+
124
+ void main();
125
+ `;
126
+ exports.mainViewTemplate = `<!-- @template name="main_view" -->
127
+ <!doctype html>
128
+ <html lang="en">
129
+ <head>
130
+ <meta charset="UTF-8">
131
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
132
+ <title>Your App</title>
133
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
134
+ <script src="/app.js"></script>
135
+ </head>
136
+ <body>
137
+ <div class="container-fluid">
138
+ <div id="main">{{ content }}</div>
139
+ </div>
140
+ </body>
141
+ </html>
142
+ `;
143
+ exports.errorTemplate = `<!-- @template name="error" -->
144
+ <!doctype html>
145
+ <html lang="en">
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
+ <title>Error - Your App</title>
150
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
151
+ <script src="/app.js"></script>
152
+ </head>
153
+ <body>
154
+ <div class="container-fluid">
155
+ <div class="row justify-content-center">
156
+ <div class="col-md-6">
157
+ <div class="text-center mt-5">
158
+ <div class="display-1 text-danger fw-bold mb-3">{{ statusCode }}</div>
159
+ <h2 class="mb-3">Oops! Something went wrong</h2>
160
+ <p class="text-muted mb-4">{{ error }}</p>
161
+ <div class="d-flex gap-2 justify-content-center">
162
+ <button class="btn btn-primary" onclick="window.history.back()">Go Back</button>
163
+ <a href="/" class="btn btn-outline-secondary">Home Page</a>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </body>
170
+ </html>
171
+ `;
172
+ exports.frontendScriptTemplate = `/**
173
+ * Common Frontend Functions for Generated Apps
174
+ * This script provides utilities for UI feedback, navigation, form handling, and SPA-like behavior
175
+ */
176
+
177
+ // Global configuration
178
+ window.AppConfig = {
179
+ toastDuration: 3000,
180
+ modalDuration: 1200,
181
+ animationDuration: 300,
182
+ debounceDelay: 300,
183
+ translations: {},
184
+ currentLang: null
185
+ };
186
+
187
+ // ===== TRANSLATION FUNCTIONS =====
188
+
189
+ /**
190
+ * Get current language
191
+ * Priority: localStorage -> navigator.language -> 'en'
192
+ * @returns {string} Current language code
193
+ */
194
+ function getCurrentLanguage() {
195
+ if (window.AppConfig.currentLang) {
196
+ return window.AppConfig.currentLang;
197
+ }
198
+
199
+ // 1. Check localStorage
200
+ const storedLang = localStorage.getItem('lang');
201
+ if (storedLang) {
202
+ window.AppConfig.currentLang = storedLang;
203
+ return storedLang;
204
+ }
205
+
206
+ // 2. Check browser language (Accept-Language equivalent)
207
+ const browserLang = navigator.language || navigator.languages?.[0];
208
+ if (browserLang) {
209
+ // Extract language code (e.g., 'en-US' -> 'en')
210
+ const langCode = browserLang.split('-')[0];
211
+ window.AppConfig.currentLang = langCode;
212
+ return langCode;
213
+ }
214
+
215
+ // 3. Default fallback
216
+ window.AppConfig.currentLang = 'en';
217
+ return 'en';
218
+ }
219
+
220
+ /**
221
+ * Translate string to current language
222
+ * @param {string} str - String in default language to translate
223
+ * @returns {string} Translated string or original if translation not found
224
+ */
225
+ function t(str) {
226
+ if (!str || typeof str !== 'string') return str;
227
+
228
+ const currentLang = getCurrentLanguage();
229
+
230
+ // If current language is the default or no translations loaded, return original
231
+ if (currentLang === 'en' || !window.AppConfig.translations || !window.AppConfig.translations[currentLang]) {
232
+ return str;
233
+ }
234
+
235
+ const translation = window.AppConfig.translations[currentLang][str];
236
+ return translation || str;
237
+ }
238
+
239
+ /**
240
+ * Set current language and save to localStorage
241
+ * @param {string} langKey - Language code (e.g., 'en', 'ru', 'pl')
242
+ */
243
+ function setLang(langKey) {
244
+ if (!langKey || typeof langKey !== 'string') return;
245
+
246
+ window.AppConfig.currentLang = langKey;
247
+ localStorage.setItem('lang', langKey);
248
+
249
+ // Optionally reload page to apply translations
250
+ // Uncomment the next line if you want automatic page reload on language change
251
+ // window.location.reload();
252
+ }
253
+
254
+ /**
255
+ * Load translations from JSON file
256
+ * @param {string} url - URL to translations JSON file (default: '/translations.json')
257
+ */
258
+ function loadTranslations(url = '/translations.json') {
259
+ fetch(url)
260
+ .then(response => {
261
+ if (!response.ok) {
262
+ console.warn('Translations file not found:', url);
263
+ return {};
264
+ }
265
+ return response.json();
266
+ })
267
+ .then(translations => {
268
+ window.AppConfig.translations = translations || {};
269
+ })
270
+ .catch(error => {
271
+ console.warn('Failed to load translations:', error);
272
+ window.AppConfig.translations = {};
273
+ });
274
+ }
275
+
276
+ // ===== UI FEEDBACK & NOTIFICATIONS =====
277
+
278
+ /**
279
+ * Show a toast notification
280
+ * @param {string} message - The message to display
281
+ * @param {string} type - 'success', 'error', 'info', 'warning'
282
+ */
283
+ function showToast(message, type = 'info') {
284
+ // Translate the message
285
+ message = t(message);
286
+ const toast = document.createElement('div');
287
+ toast.className = 'app-toast app-toast-' + type;
288
+ toast.textContent = message;
289
+ toast.style.cssText = \`
290
+ position: fixed;
291
+ top: 20px;
292
+ right: 20px;
293
+ padding: 12px 24px;
294
+ border-radius: 4px;
295
+ color: white;
296
+ font-weight: 500;
297
+ z-index: 10000;
298
+ max-width: 300px;
299
+ word-wrap: break-word;
300
+ transition: all \${window.AppConfig.animationDuration}ms ease;
301
+ transform: translateX(100%);
302
+ opacity: 0;
303
+ \`;
304
+
305
+ // Type-specific styling
306
+ const colors = {
307
+ success: '#10b981',
308
+ error: '#ef4444',
309
+ warning: '#f59e0b',
310
+ info: '#3b82f6'
311
+ };
312
+ toast.style.backgroundColor = colors[type] || colors.info;
313
+
314
+ document.body.appendChild(toast);
315
+
316
+ // Animate in
317
+ setTimeout(() => {
318
+ toast.style.transform = 'translateX(0)';
319
+ toast.style.opacity = '1';
320
+ }, 10);
321
+
322
+ // Auto remove
323
+ setTimeout(() => {
324
+ toast.style.transform = 'translateX(100%)';
325
+ toast.style.opacity = '0';
326
+ setTimeout(() => {
327
+ if (toast.parentNode) {
328
+ toast.parentNode.removeChild(toast);
329
+ }
330
+ }, window.AppConfig.animationDuration);
331
+ }, window.AppConfig.toastDuration);
332
+ }
333
+
334
+ /**
335
+ * Display inline message in specific container
336
+ * @param {string} elementId - ID of the target element
337
+ * @param {string} message - The message to display
338
+ * @param {string} type - 'success', 'error', 'info', 'warning'
339
+ */
340
+ function showMessage(elementId, message, type = 'info') {
341
+ const element = getElementSafely('#' + elementId);
342
+ if (!element) return;
343
+
344
+ // Translate the message
345
+ message = t(message);
346
+ element.textContent = message;
347
+ element.className = 'app-message app-message-' + type;
348
+ element.style.cssText = \`
349
+ padding: 8px 12px;
350
+ border-radius: 4px;
351
+ margin: 8px 0;
352
+ font-size: 14px;
353
+ display: block;
354
+ \`;
355
+
356
+ // Type-specific styling
357
+ const styles = {
358
+ success: 'background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0;',
359
+ error: 'background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5;',
360
+ warning: 'background: #fef3c7; color: #92400e; border: 1px solid #fcd34d;',
361
+ info: 'background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd;'
362
+ };
363
+ element.style.cssText += styles[type] || styles.info;
364
+ }
365
+
366
+ /**
367
+ * Show modal dialog
368
+ * @param {string} modalId - ID of the modal element
369
+ * @param {string} message - The message to display
370
+ * @param {string} type - 'success', 'error', 'info', 'warning'
371
+ */
372
+ function showModal(modalId, message, type = 'info') {
373
+ let modal = getElementSafely('#' + modalId);
374
+
375
+ if (!modal) {
376
+ // Create modal if it doesn't exist
377
+ modal = document.createElement('dialog');
378
+ modal.id = modalId;
379
+ modal.innerHTML = \`
380
+ <div class="modal-content">
381
+ <div class="modal-header">
382
+ <button class="modal-close" onclick="this.closest('dialog').close()">&times;</button>
383
+ </div>
384
+ <div class="modal-body"></div>
385
+ </div>
386
+ \`;
387
+ modal.style.cssText = \`
388
+ border: none;
389
+ border-radius: 8px;
390
+ padding: 0;
391
+ max-width: 400px;
392
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
393
+ \`;
394
+ document.body.appendChild(modal);
395
+ }
396
+
397
+ const content = modal.querySelector('.modal-body');
398
+ if (content) {
399
+ // Translate the message
400
+ message = t(message);
401
+ content.textContent = message;
402
+ content.className = 'modal-body modal-' + type;
403
+ content.style.cssText = 'padding: 20px; text-align: center;';
404
+ }
405
+
406
+ if (modal.showModal) {
407
+ modal.showModal();
408
+ setTimeout(() => {
409
+ try { modal.close(); } catch(e) {}
410
+ }, window.AppConfig.modalDuration);
411
+ }
412
+ }
413
+
414
+ // ===== NAVIGATION & PAGE ACTIONS =====
415
+
416
+ /**
417
+ * Enhanced history.back() with fallback
418
+ */
419
+ function navigateBack() {
420
+ if (window.history.length > 1) {
421
+ window.history.back();
422
+ } else {
423
+ window.location.href = '/';
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Safe redirect with validation
429
+ * @param {string} url - The URL to redirect to
430
+ */
431
+ function redirectTo(url) {
432
+ if (!url || typeof url !== 'string') return;
433
+ // Basic URL validation
434
+ if (url.startsWith('/') || url.startsWith('http')) {
435
+ window.location.href = url;
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Page reload with loading indication
441
+ */
442
+ function reloadPage() {
443
+ showToast('Reloading page...', 'info');
444
+ setTimeout(() => {
445
+ window.location.reload();
446
+ }, 500);
447
+ }
448
+
449
+ /**
450
+ * Refresh specific page sections by reloading their content
451
+ * @param {string} selector - CSS selector for the section to refresh
452
+ */
453
+ function refreshSection(selector) {
454
+ const element = getElementSafely(selector);
455
+ if (!element) return;
456
+
457
+ // If element has data-refresh-url, use that to reload content
458
+ const refreshUrl = element.getAttribute('data-refresh-url');
459
+ if (refreshUrl) {
460
+ fetch(refreshUrl)
461
+ .then(response => response.text())
462
+ .then(html => {
463
+ element.innerHTML = html;
464
+ })
465
+ .catch(error => {
466
+ console.error('Failed to refresh section:', error);
467
+ showToast('Failed to refresh content', 'error');
468
+ });
469
+ }
470
+ }
471
+
472
+ // ===== FORM & CONTENT MANAGEMENT =====
473
+
474
+ /**
475
+ * Safe element removal with animation
476
+ * @param {string} selector - CSS selector for element to remove
477
+ */
478
+ function removeElement(selector) {
479
+ const element = getElementSafely(selector);
480
+ if (!element) return;
481
+
482
+ element.style.transition = \`opacity \${window.AppConfig.animationDuration}ms ease\`;
483
+ element.style.opacity = '0';
484
+
485
+ setTimeout(() => {
486
+ if (element.parentNode) {
487
+ element.parentNode.removeChild(element);
488
+ }
489
+ }, window.AppConfig.animationDuration);
490
+ }
491
+
492
+ /**
493
+ * Update content in element
494
+ * @param {string} selector - CSS selector for target element
495
+ * @param {string} content - New content
496
+ * @param {string} mode - 'replace', 'append', 'prepend'
497
+ */
498
+ function updateContent(selector, content, mode = 'replace') {
499
+ const element = getElementSafely(selector);
500
+ if (!element) return;
501
+
502
+ switch (mode) {
503
+ case 'append':
504
+ element.innerHTML += content;
505
+ break;
506
+ case 'prepend':
507
+ element.innerHTML = content + element.innerHTML;
508
+ break;
509
+ case 'replace':
510
+ default:
511
+ element.innerHTML = content;
512
+ break;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Reset form fields and validation states
518
+ * @param {string} formSelector - CSS selector for the form
519
+ */
520
+ function clearForm(formSelector) {
521
+ const form = getElementSafely(formSelector);
522
+ if (!form) return;
523
+
524
+ if (form.reset) {
525
+ form.reset();
526
+ }
527
+
528
+ // Clear validation messages
529
+ const messages = form.querySelectorAll('.app-message');
530
+ messages.forEach(msg => msg.remove());
531
+ }
532
+
533
+ // ===== CUSTOM SPA INTEGRATION =====
534
+
535
+ /**
536
+ * Centralized success handler that executes strategy array
537
+ * @param {object} response - The response object
538
+ * @param {string[]} strategy - Array of strategy actions
539
+ * @param {object} options - Additional options (basePath, entityName, etc.)
540
+ */
541
+ function handleFormSuccess(response, strategy = ['back', 'toast'], options = {}) {
542
+ const { basePath, entityName = 'Item' } = options;
543
+
544
+ strategy.forEach(action => {
545
+ switch (action) {
546
+ case 'toast':
547
+ showToast(\`\${entityName} saved successfully\`, 'success');
548
+ break;
549
+ case 'message':
550
+ if (options.messageId) {
551
+ showMessage(options.messageId, \`\${entityName} saved successfully\`, 'success');
552
+ }
553
+ break;
554
+ case 'modal':
555
+ if (options.modalId) {
556
+ showModal(options.modalId, \`\${entityName} saved successfully\`, 'success');
557
+ }
558
+ break;
559
+ case 'remove':
560
+ // If targetSelector is specified, remove that element instead of the form
561
+ if (options.targetSelector) {
562
+ removeElement(options.targetSelector);
563
+ } else if (options.formSelector) {
564
+ removeElement(options.formSelector);
565
+ }
566
+ break;
567
+ case 'redirect':
568
+ if (basePath) {
569
+ redirectTo(basePath);
570
+ }
571
+ break;
572
+ case 'back':
573
+ navigateBack();
574
+ break;
575
+ case 'reload':
576
+ case 'refresh':
577
+ reloadPage();
578
+ break;
579
+ }
580
+ });
581
+ }
582
+
583
+ /**
584
+ * Handle internal link navigation via fetch
585
+ * @param {string} url - The URL to navigate to
586
+ * @param {Element} targetElement - Element to update with new content (default: #main)
587
+ */
588
+ function navigateToPage(url, targetElement = null) {
589
+ const target = targetElement || document.querySelector('#main');
590
+ if (!target) return;
591
+
592
+ showLoading('#main');
593
+
594
+ fetch(url, {
595
+ headers: {
596
+ 'Accept': 'text/html',
597
+ 'X-Partial-Content': 'true'
598
+ }
599
+ })
600
+ .then(response => {
601
+ if (!response.ok) {
602
+ throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
603
+ }
604
+ return response.text();
605
+ })
606
+ .then(html => {
607
+ target.innerHTML = html;
608
+ // Update browser history
609
+ window.history.pushState({}, '', url);
610
+ // Re-initialize event listeners for new content
611
+ initializeEventListeners();
612
+ })
613
+ .catch(error => {
614
+ console.error('Navigation failed:', error);
615
+ showToast('Failed to load page', 'error');
616
+ // Fallback to normal navigation
617
+ window.location.href = url;
618
+ })
619
+ .finally(() => {
620
+ hideLoading('#main');
621
+ });
622
+ }
623
+
624
+ /**
625
+ * Convert form value to appropriate type based on field type
626
+ * @param {string} value - Raw form value
627
+ * @param {string} fieldType - Field type (number, boolean, etc.)
628
+ * @returns {any} Converted value
629
+ */
630
+ function convertFieldValue(value, fieldType) {
631
+ if (!value || value === '') {
632
+ return null;
633
+ }
634
+
635
+ switch (fieldType.toLowerCase()) {
636
+ case 'number':
637
+ case 'int':
638
+ case 'integer':
639
+ const intVal = parseInt(value, 10);
640
+ return isNaN(intVal) ? null : intVal;
641
+
642
+ case 'float':
643
+ case 'decimal':
644
+ const floatVal = parseFloat(value);
645
+ return isNaN(floatVal) ? null : floatVal;
646
+
647
+ case 'boolean':
648
+ case 'bool':
649
+ if (value === 'true') return true;
650
+ if (value === 'false') return false;
651
+ return Boolean(value);
652
+
653
+ case 'enum':
654
+ case 'string':
655
+ case 'text':
656
+ default:
657
+ return value;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Handle form submission via fetch with JSON data
663
+ * @param {HTMLFormElement} form - The form element
664
+ * @param {string[]} strategy - Strategy actions to execute on success
665
+ * @param {object} options - Additional options
666
+ */
667
+ function submitForm(form, strategy = ['back', 'toast'], options = {}) {
668
+ const formData = new FormData(form);
669
+ const jsonData = {};
670
+
671
+ // Get field types from form data attribute
672
+ const fieldTypesAttr = form.getAttribute('data-field-types');
673
+ const fieldTypes = fieldTypesAttr ? JSON.parse(fieldTypesAttr) : {};
674
+
675
+ // Convert FormData to JSON with proper typing
676
+ for (const [key, value] of formData.entries()) {
677
+ const fieldType = fieldTypes[key] || 'string';
678
+ const convertedValue = convertFieldValue(value, fieldType);
679
+
680
+ if (jsonData[key]) {
681
+ // Handle multiple values (e.g., checkboxes)
682
+ if (Array.isArray(jsonData[key])) {
683
+ jsonData[key].push(convertedValue);
684
+ } else {
685
+ jsonData[key] = [jsonData[key], convertedValue];
686
+ }
687
+ } else {
688
+ jsonData[key] = convertedValue;
689
+ }
690
+ }
691
+
692
+ const url = form.getAttribute('data-action') || form.action;
693
+ const method = (form.getAttribute('data-method') || form.method || 'POST').toUpperCase();
694
+
695
+ showLoading(form);
696
+
697
+ fetch(url, {
698
+ method,
699
+ headers: {
700
+ 'Content-Type': 'application/json',
701
+ 'Accept': 'application/json'
702
+ },
703
+ body: JSON.stringify(jsonData)
704
+ })
705
+ .then(response => {
706
+ if (!response.ok) {
707
+ throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
708
+ }
709
+ return response.json();
710
+ })
711
+ .then(data => {
712
+ handleFormSuccess(data, strategy, options);
713
+ })
714
+ .catch(error => {
715
+ console.error('Form submission failed:', error);
716
+ handleFormError({ message: error.message || 'Form submission failed' }, options);
717
+ })
718
+ .finally(() => {
719
+ hideLoading(form);
720
+ });
721
+ }
722
+
723
+ /**
724
+ * Standardized error handling for forms
725
+ * @param {object} response - The error response
726
+ * @param {object} options - Options including target elements
727
+ */
728
+ function handleFormError(response, options = {}) {
729
+ const message = response.message || 'An error occurred';
730
+
731
+ if (options.messageId) {
732
+ showMessage(options.messageId, message, 'error');
733
+ } else {
734
+ showToast(message, 'error');
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Add client-side validation helpers
740
+ * @param {string} formSelector - CSS selector for the form
741
+ */
742
+ function setupFormValidation(formSelector) {
743
+ const form = getElementSafely(formSelector);
744
+ if (!form) return;
745
+
746
+ // Add basic required field validation
747
+ const requiredFields = form.querySelectorAll('[required]');
748
+ requiredFields.forEach(field => {
749
+ field.addEventListener('blur', function() {
750
+ if (!this.value.trim()) {
751
+ showMessage(this.id + '-error', 'This field is required', 'error');
752
+ } else {
753
+ const errorEl = getElementSafely('#' + this.id + '-error');
754
+ if (errorEl) errorEl.textContent = '';
755
+ }
756
+ });
757
+ });
758
+ }
759
+
760
+ // ===== UTILITY FUNCTIONS =====
761
+
762
+ /**
763
+ * Safe element selection with error handling
764
+ * @param {string} selector - CSS selector
765
+ * @returns {Element|null}
766
+ */
767
+ function getElementSafely(selector) {
768
+ try {
769
+ return document.querySelector(selector);
770
+ } catch (e) {
771
+ console.warn('Invalid selector:', selector);
772
+ return null;
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Debounce utility for search/input handlers
778
+ * @param {Function} fn - Function to debounce
779
+ * @param {number} delay - Delay in milliseconds
780
+ * @returns {Function}
781
+ */
782
+ function debounce(fn, delay = window.AppConfig.debounceDelay) {
783
+ let timeoutId;
784
+ return function(...args) {
785
+ clearTimeout(timeoutId);
786
+ timeoutId = setTimeout(() => fn.apply(this, args), delay);
787
+ };
788
+ }
789
+
790
+ /**
791
+ * Show loading state for target element
792
+ * @param {string} target - CSS selector for target element
793
+ */
794
+ function showLoading(target) {
795
+ const element = getElementSafely(target);
796
+ if (!element) return;
797
+
798
+ element.style.position = 'relative';
799
+ element.style.pointerEvents = 'none';
800
+ element.style.opacity = '0.6';
801
+
802
+ const loader = document.createElement('div');
803
+ loader.className = 'app-loader';
804
+ loader.style.cssText = \`
805
+ position: absolute;
806
+ top: 50%;
807
+ left: 50%;
808
+ transform: translate(-50%, -50%);
809
+ width: 20px;
810
+ height: 20px;
811
+ border: 2px solid #f3f3f3;
812
+ border-top: 2px solid #3498db;
813
+ border-radius: 50%;
814
+ animation: spin 1s linear infinite;
815
+ z-index: 1000;
816
+ \`;
817
+
818
+ element.appendChild(loader);
819
+
820
+ // Add CSS animation if not exists
821
+ if (!document.querySelector('#app-loader-styles')) {
822
+ const style = document.createElement('style');
823
+ style.id = 'app-loader-styles';
824
+ style.textContent = \`
825
+ @keyframes spin {
826
+ 0% { transform: translate(-50%, -50%) rotate(0deg); }
827
+ 100% { transform: translate(-50%, -50%) rotate(360deg); }
828
+ }
829
+ \`;
830
+ document.head.appendChild(style);
831
+ }
832
+ }
833
+
834
+ /**
835
+ * Hide loading state for target element
836
+ * @param {string} target - CSS selector for target element
837
+ */
838
+ function hideLoading(target) {
839
+ const element = getElementSafely(target);
840
+ if (!element) return;
841
+
842
+ element.style.pointerEvents = '';
843
+ element.style.opacity = '';
844
+
845
+ const loader = element.querySelector('.app-loader');
846
+ if (loader) {
847
+ loader.remove();
848
+ }
849
+ }
850
+
851
+ // ===== INITIALIZATION =====
852
+
853
+ /**
854
+ * Initialize event listeners for links and forms
855
+ */
856
+ function initializeEventListeners() {
857
+ // Handle internal links
858
+ document.querySelectorAll('a[href]').forEach(link => {
859
+ const href = link.getAttribute('href');
860
+
861
+ // Skip external links, anchors, and special protocols
862
+ if (!href ||
863
+ href.startsWith('http://') ||
864
+ href.startsWith('https://') ||
865
+ href.startsWith('mailto:') ||
866
+ href.startsWith('tel:') ||
867
+ href.startsWith('#') ||
868
+ href.startsWith('javascript:')) {
869
+ return;
870
+ }
871
+
872
+ // Remove any existing event listeners and add new one
873
+ link.removeEventListener('click', handleLinkClick);
874
+ link.addEventListener('click', handleLinkClick);
875
+ });
876
+
877
+ // Handle forms with strategy
878
+ document.querySelectorAll('form[data-strategy]').forEach(form => {
879
+ // Remove any existing event listeners and add new one
880
+ form.removeEventListener('submit', handleFormSubmit);
881
+ form.addEventListener('submit', handleFormSubmit);
882
+ });
883
+ }
884
+
885
+ /**
886
+ * Handle link click for internal navigation
887
+ * @param {Event} event - Click event
888
+ */
889
+ function handleLinkClick(event) {
890
+ event.preventDefault();
891
+ const href = event.currentTarget.getAttribute('href');
892
+ if (href) {
893
+ navigateToPage(href);
894
+ }
895
+ }
896
+
897
+ /**
898
+ * Handle form submission
899
+ * @param {Event} event - Submit event
900
+ */
901
+ function handleFormSubmit(event) {
902
+ event.preventDefault();
903
+ const form = event.target;
904
+
905
+ // Check for confirmation message
906
+ const confirmMessage = form.getAttribute('data-confirm-message');
907
+ if (confirmMessage) {
908
+ const confirmed = confirm(t(confirmMessage));
909
+ if (!confirmed) {
910
+ return; // User cancelled
911
+ }
912
+ }
913
+
914
+ const strategyAttr = form.getAttribute('data-strategy');
915
+ const strategy = strategyAttr ? JSON.parse(strategyAttr) : ['back', 'toast'];
916
+
917
+ // Extract options from form data attributes
918
+ const options = {
919
+ basePath: form.getAttribute('data-base-path') || '',
920
+ entityName: form.getAttribute('data-entity-name') || 'Item',
921
+ messageId: form.getAttribute('data-message-id'),
922
+ modalId: form.getAttribute('data-modal-id'),
923
+ formSelector: \`form[data-template="\${form.getAttribute('data-template')}"]\`,
924
+ targetSelector: form.getAttribute('data-target-selector')
925
+ };
926
+
927
+ submitForm(form, strategy, options);
928
+ }
929
+
930
+ // Set up event handlers on page load
931
+ document.addEventListener('DOMContentLoaded', function() {
932
+ // Initialize event listeners
933
+ initializeEventListeners();
934
+
935
+ // Handle browser back/forward buttons
936
+ window.addEventListener('popstate', function(event) {
937
+ // Reload the page content for the current URL
938
+ navigateToPage(window.location.pathname);
939
+ });
940
+
941
+ // Load translations on page load
942
+ loadTranslations();
943
+ });
944
+
945
+ // Expose functions globally
946
+ window.App = {
947
+ // Translation functions
948
+ t,
949
+ setLang,
950
+ getCurrentLanguage,
951
+ loadTranslations,
952
+ showToast,
953
+ showMessage,
954
+ showModal,
955
+ navigateBack,
956
+ redirectTo,
957
+ reloadPage,
958
+ refreshSection,
959
+ removeElement,
960
+ updateContent,
961
+ clearForm,
962
+ handleFormSuccess,
963
+ handleFormError,
964
+ setupFormValidation,
965
+ getElementSafely,
966
+ debounce,
967
+ showLoading,
968
+ hideLoading,
969
+ // New SPA functions
970
+ navigateToPage,
971
+ submitForm,
972
+ initializeEventListeners,
973
+ // Type conversion
974
+ convertFieldValue
975
+ };
976
+ `;
977
+ exports.translationsTemplate = `{
978
+ "ru": {
979
+ "error": "ошибка",
980
+ "success": "успешно",
981
+ "Reloading page...": "Перезагрузка страницы...",
982
+ "Request failed. Please try again.": "Запрос не выполнен. Попробуйте еще раз.",
983
+ "This field is required": "Это поле обязательно",
984
+ "An error occurred": "Произошла ошибка",
985
+ "saved successfully": "успешно сохранено",
986
+ "updated successfully": "успешно обновлено",
987
+ "deleted successfully": "успешно удалено"
988
+ },
989
+ "pl": {
990
+ "error": "błąd",
991
+ "success": "sukces",
992
+ "Reloading page...": "Przeładowywanie strony...",
993
+ "Request failed. Please try again.": "Żądanie nie powiodło się. Spróbuj ponownie.",
994
+ "This field is required": "To pole jest wymagane",
995
+ "An error occurred": "Wystąpił błąd",
996
+ "saved successfully": "zapisano pomyślnie",
997
+ "updated successfully": "zaktualizowano pomyślnie",
998
+ "deleted successfully": "usunięto pomyślnie"
999
+ },
1000
+ "es": {
1001
+ "error": "error",
1002
+ "success": "éxito",
1003
+ "Reloading page...": "Recargando página...",
1004
+ "Request failed. Please try again.": "La solicitud falló. Por favor, inténtelo de nuevo.",
1005
+ "This field is required": "Este campo es obligatorio",
1006
+ "An error occurred": "Ha ocurrido un error",
1007
+ "saved successfully": "guardado exitosamente",
1008
+ "updated successfully": "actualizado exitosamente",
1009
+ "deleted successfully": "eliminado exitosamente"
1010
+ },
1011
+ "de": {
1012
+ "error": "Fehler",
1013
+ "success": "Erfolg",
1014
+ "Reloading page...": "Seite wird neu geladen...",
1015
+ "Request failed. Please try again.": "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut.",
1016
+ "This field is required": "Dieses Feld ist erforderlich",
1017
+ "An error occurred": "Ein Fehler ist aufgetreten",
1018
+ "saved successfully": "erfolgreich gespeichert",
1019
+ "updated successfully": "erfolgreich aktualisiert",
1020
+ "deleted successfully": "erfolgreich gelöscht"
1021
+ },
1022
+ "pt": {
1023
+ "error": "erro",
1024
+ "success": "sucesso",
1025
+ "Reloading page...": "Recarregando página...",
1026
+ "Request failed. Please try again.": "A solicitação falhou. Por favor, tente novamente.",
1027
+ "This field is required": "Este campo é obrigatório",
1028
+ "An error occurred": "Ocorreu um erro",
1029
+ "saved successfully": "salvo com sucesso",
1030
+ "updated successfully": "atualizado com sucesso",
1031
+ "deleted successfully": "excluído com sucesso"
1032
+ },
1033
+ "zh": {
1034
+ "error": "错误",
1035
+ "success": "成功",
1036
+ "Reloading page...": "正在重新加载页面...",
1037
+ "Request failed. Please try again.": "请求失败。请再试一次。",
1038
+ "This field is required": "此字段为必填项",
1039
+ "An error occurred": "发生错误",
1040
+ "saved successfully": "保存成功",
1041
+ "updated successfully": "更新成功",
1042
+ "deleted successfully": "删除成功"
1043
+ }
1044
+ }`;
1045
+ exports.cursorRulesTemplate = `# CurrentJS Framework Rules
1046
+
1047
+ ## Architecture Overview
1048
+ This is a CurrentJS framework application using clean architecture principles with the following layers:
1049
+ - **Controllers**: Handle HTTP requests/responses and route handling
1050
+ - **Services**: Contain business logic and orchestrate operations
1051
+ - **Stores**: Provide data access layer and database operations
1052
+ - **Domain Entities**: Core business models
1053
+ - **Views**: HTML templates for server-side rendering
1054
+
1055
+ ## Commands
1056
+
1057
+ \`\`\`bash
1058
+ current create module Modulename # Creates "Modulename" with default structure and yaml file
1059
+ \`\`\`
1060
+ \`\`\`bash
1061
+ current generate Modulename # Generates all TypeScript files based on the module's yaml
1062
+ current generate # Does the same as above, but for all modules
1063
+ \`\`\`
1064
+ \`\`\`bash
1065
+ current commit [files...] # Commits all changes in the code, so they won't be overwritten after regeneration
1066
+ current diff [module] # Show differences between generated and current code
1067
+ \`\`\`
1068
+
1069
+ ## The flow
1070
+ 1. Create an empty app (\`current create app\`) – this step is already done.
1071
+ 2. Create a new module: \`current create module Name\`
1072
+ 3. In the module's yaml file, define module's:
1073
+ - model(s)
1074
+ - routes & actions
1075
+ - permissions
1076
+ 4. Generate TypeScript files: \`current generate Name\`
1077
+ 5. If required, make changes in the:
1078
+ - model (i.e. some specific business rules or validations)
1079
+ - views
1080
+ 6. Commit those changes: \`current commit\`
1081
+
1082
+ --- If needed more than CRUD: ---
1083
+
1084
+ 7. Define action in the service by creating a method
1085
+ 8. Describe this action in the module's yaml. Additionaly, you may define routes and permissions.
1086
+ 9. \`current generate Modulename\`
1087
+ 10. commit changes: \`current commit\`
1088
+
1089
+ ## Configuration Files
1090
+
1091
+ ### Application Configuration (app.yaml)
1092
+
1093
+ **Do not modify this file**
1094
+
1095
+ ### Module Configuration (modulename.yaml)
1096
+
1097
+ **The most work must be done in these files**
1098
+
1099
+ **Complete Module Example:**
1100
+ \`\`\`yaml
1101
+ models:
1102
+ - name: Post # Entity name (capitalized)
1103
+ fields:
1104
+ - name: title # Field name
1105
+ type: string # Field type: string, number, boolean, datetime
1106
+ required: true # Validation requirement
1107
+ - name: content
1108
+ type: string
1109
+ required: true
1110
+ - name: authorId
1111
+ type: number
1112
+ required: true
1113
+ - name: publishedAt
1114
+ type: datetime
1115
+ required: false
1116
+ - name: status
1117
+ type: string
1118
+ required: true
1119
+
1120
+ api: # REST API configuration
1121
+ prefix: /api/posts # Base URL for API endpoints
1122
+ endpoints:
1123
+ - method: GET # HTTP method
1124
+ path: / # Relative path (becomes /api/posts/)
1125
+ action: list # Action name (references actions section)
1126
+ - method: GET
1127
+ path: /:id # Path parameter
1128
+ action: get
1129
+ - method: POST
1130
+ path: /
1131
+ action: create
1132
+ - method: PUT
1133
+ path: /:id
1134
+ action: update
1135
+ - method: DELETE
1136
+ path: /:id
1137
+ action: delete
1138
+ - method: POST # Custom endpoint
1139
+ path: /:id/publish
1140
+ action: publish
1141
+
1142
+ routes: # Web interface configuration
1143
+ prefix: /posts # Base URL for web pages
1144
+ strategy: [toast, back] # Default success strategies for forms
1145
+ endpoints:
1146
+ - path: / # List page
1147
+ action: list
1148
+ view: postList # Template name
1149
+ - path: /:id # Detail page
1150
+ action: get
1151
+ view: postDetail
1152
+ - path: /create # Create form page
1153
+ action: empty # No data loading action
1154
+ view: postCreate
1155
+ - path: /:id/edit # Edit form page
1156
+ action: get # Load existing data
1157
+ view: postUpdate
1158
+
1159
+ actions: # Business logic mapping
1160
+ list:
1161
+ handlers: [default:list] # Use built-in list handler
1162
+ get:
1163
+ handlers: [default:getById] # Use built-in get handler
1164
+ create:
1165
+ handlers: [default:create] # Built-in create
1166
+ update:
1167
+ handlers: [default:update]
1168
+ delete:
1169
+ handlers: [ # Chain multiple handlers
1170
+ service:checkCanDelete, # Custom business logic
1171
+ default:delete
1172
+ ]
1173
+ publish: # Custom action
1174
+ handlers: [
1175
+ default:getById, # Fetch entity
1176
+ service:validateForPublish, # Custom validation
1177
+ service:updatePublishStatus # Custom update logic
1178
+ ]
1179
+
1180
+ permissions: # Role-based access control
1181
+ - action: list
1182
+ roles: [all] # Anyone (including anonymous)
1183
+ - action: get
1184
+ roles: [all] # Anyone can view
1185
+ - action: create
1186
+ roles: [authenticated] # Must be logged in
1187
+ - action: update
1188
+ roles: [owner, admin] # Entity owner or admin role
1189
+ - action: delete
1190
+ roles: [admin] # Only admin role
1191
+ - action: publish
1192
+ roles: [owner, editor, admin] # Multiple roles allowed
1193
+ \`\`\`
1194
+
1195
+ **Make sure no \`ID\`/\`owner id\`/\`is deleted\` fields are present in the model definition, since it's added automatically**
1196
+
1197
+ **Field Types:**
1198
+ - \`string\` - Text data
1199
+ - \`number\` - Numeric data (integer or float)
1200
+ - \`boolean\` - True/false values
1201
+ - \`datetime\` - Date and time values
1202
+
1203
+ **Built-in Action Handlers:**
1204
+ - \`default:list\` - Returns paginated list of entities
1205
+ - \`default:getById\` - Fetches single entity by ID
1206
+ - \`default:create\` - Creates new entity with validation
1207
+ - \`default:update\` - Updates existing entity by ID
1208
+ - \`default:delete\` - Soft deletes entity
1209
+
1210
+ **Custom Action Handlers:**
1211
+ - \`service:methodName\` - Calls custom method on service class
1212
+ - Automatically generated with proper type signatures
1213
+ - Can be chained with built-in handlers
1214
+
1215
+ **Strategy Options (for forms):**
1216
+ - \`toast\` - Success toast notification
1217
+ - \`back\` - Navigate back in browser history
1218
+ - \`message\` - Inline success message
1219
+ - \`modal\` - Modal success dialog
1220
+ - \`redirect\` - Redirect to specific URL
1221
+ - \`refresh\` - Reload current page
1222
+
1223
+ **Permission Roles:**
1224
+ - \`all\` - Anyone (including anonymous users)
1225
+ - \`authenticated\` - Any logged-in user
1226
+ - \`owner\` - User who created the entity
1227
+ - \`admin\`, \`editor\`, \`user\` - Custom roles from JWT token
1228
+ - Multiple roles can be specified for each action
1229
+
1230
+ **Generated Files from Configuration:**
1231
+ - Domain entity class
1232
+ - Service class
1233
+ - API controller with REST endpoints
1234
+ - Web controller with page rendering
1235
+ - Store class with database operations
1236
+ - HTML templates for all views
1237
+ - TypeScript interfaces and DTOs
1238
+
1239
+ ## Module Structure
1240
+ \`\`\`
1241
+ src/modules/ModuleName/
1242
+ ├── application/
1243
+ │ ├── services/ModuleService.ts # Business logic
1244
+ │ └── validation/ModuleValidation.ts # DTOs and validation
1245
+ ├── domain/
1246
+ │ └── entities/Module.ts # Domain model
1247
+ ├── infrastructure/
1248
+ │ ├── controllers/
1249
+ │ │ ├── ModuleApiController.ts # REST API endpoints
1250
+ │ │ └── ModuleWebController.ts # Web page controllers
1251
+ │ ├── interfaces/StoreInterface.ts # Data access interface
1252
+ │ └── stores/ModuleStore.ts # Data access implementation
1253
+ ├── views/
1254
+ │ ├── modulelist.html # List view template
1255
+ │ ├── moduledetail.html # Detail view template
1256
+ │ ├── modulecreate.html # Create form template
1257
+ │ └── moduleupdate.html # Update form template
1258
+ └── module.yaml # Module configuration
1259
+ \`\`\`
1260
+
1261
+ ## Best Practices
1262
+
1263
+ - Use Domain Driven Design and Clean Architecture (kind of).
1264
+ - Prefer declarative configuration over imperative programming: when possible, change yamls instead of writing the code. Write the code only when it's really neccessary.
1265
+ - CRUD operation are autogenerated (first in module's yaml, then in the generated code).
1266
+ - If some custom action is needed, then it has to be defined in the **Service** then just put this action to the module's yaml. You may also define new methods in the *Store* and its interface (if needed).
1267
+ - Business rules must be defined only in models. That also applies to business rules validations (in contrast to *just* validations: e.g. if field exists and is of needed type – then validators are in use)
1268
+
1269
+ ## Core Package APIs (For Generated Code)
1270
+
1271
+ ### @currentjs/router - Controller Decorators & Context
1272
+
1273
+ **Decorators (in controllers):**
1274
+ \`\`\`typescript
1275
+ @Controller('/api/posts') // Base path for controller
1276
+ @Get('/') // GET endpoint
1277
+ @Get('/:id') // GET with path parameter
1278
+ @Post('/') // POST endpoint
1279
+ @Put('/:id') // PUT endpoint
1280
+ @Delete('/:id') // DELETE endpoint
1281
+ @Render('template', 'layout') // For web controllers
1282
+ \`\`\`
1283
+
1284
+ **Context Object (in route handlers):**
1285
+ \`\`\`typescript
1286
+ interface IContext {
1287
+ request: {
1288
+ url: string;
1289
+ path: string;
1290
+ method: string;
1291
+ parameters: Record<string, string | number>; // Path params + query params
1292
+ body: any; // Parsed JSON or raw string
1293
+ headers: Record<string, string | string[]>;
1294
+ user?: AuthenticatedUser; // Parsed JWT user if authenticated
1295
+ };
1296
+ response: Record<string, any>;
1297
+ }
1298
+
1299
+ // Usage examples
1300
+ const id = parseInt(ctx.request.parameters.id as string);
1301
+ const page = parseInt(ctx.request.parameters.page as string) || 1;
1302
+ const payload = ctx.request.body;
1303
+ const user = ctx.request.user; // If authenticated
1304
+ \`\`\`
1305
+
1306
+ **Authentication Support:**
1307
+ - JWT tokens parsed automatically from \`Authorization: Bearer <token>\` header
1308
+ - User object available at \`ctx.request.user\` with \`id\`, \`email\`, \`role\` fields
1309
+ - No manual setup required in generated code
1310
+
1311
+ ### @currentjs/templating - Template Syntax
1312
+
1313
+ **Variables & Data Access:**
1314
+ \`\`\`html
1315
+ {{ variableName }}
1316
+ {{ object.property }}
1317
+ {{ $root.arrayData }} <!-- Access root data -->
1318
+ {{ $index }} <!-- Loop index -->
1319
+ \`\`\`
1320
+
1321
+ **Control Structures:**
1322
+ \`\`\`html
1323
+ <!-- Loops -->
1324
+ <tbody x-for="$root" x-row="item">
1325
+ <tr>
1326
+ <td>{{ item.name }}</td>
1327
+ <td>{{ $index }}</td>
1328
+ </tr>
1329
+ </tbody>
1330
+
1331
+ <!-- Conditionals -->
1332
+ <div x-if="user.isAdmin">Admin content</div>
1333
+ <span x-if="errors.name">{{ errors.name }}</span>
1334
+
1335
+ <!-- Template includes -->
1336
+ <userCard name="{{ user.name }}" role="admin" />
1337
+ \`\`\`
1338
+
1339
+ **Layout Integration:**
1340
+ - Templates use \`<!-- @template name="templateName" -->\` header
1341
+ - Main layout gets \`{{ content }}\` variable
1342
+ - Forms use \`{{ formData.field || '' }}\` for default values
1343
+
1344
+ ### @currentjs/provider-mysql - Database Operations
1345
+
1346
+ **Query Execution (in stores):**
1347
+ \`\`\`typescript
1348
+ // Named parameters (preferred)
1349
+ const result = await this.db.query(
1350
+ 'SELECT * FROM users WHERE status = :status AND age > :minAge',
1351
+ { status: 'active', minAge: 18 }
1352
+ );
1353
+
1354
+ // Result handling
1355
+ if (result.success && result.data.length > 0) {
1356
+ return result.data.map(row => this.rowToModel(row));
1357
+ }
1358
+ \`\`\`
1359
+
1360
+ **Common Query Patterns:**
1361
+ \`\`\`typescript
1362
+ // Insert with auto-generated fields
1363
+ const row = { ...data, created_at: new Date(), updated_at: new Date() };
1364
+ const result = await this.db.query(
1365
+ \`INSERT INTO table_name (\${fields.join(', ')}) VALUES (\${placeholders})\`,
1366
+ row
1367
+ );
1368
+ const newId = result.insertId;
1369
+
1370
+ // Update with validation
1371
+ const query = \`UPDATE table_name SET \${updateFields.join(', ')}, updated_at = :updated_at WHERE id = :id\`;
1372
+ await this.db.query(query, { ...updateData, updated_at: new Date(), id });
1373
+
1374
+ // Soft delete
1375
+ await this.db.query(
1376
+ 'UPDATE table_name SET deleted_at = :deleted_at WHERE id = :id',
1377
+ { deleted_at: new Date(), id }
1378
+ );
1379
+ \`\`\`
1380
+
1381
+ **Error Handling:**
1382
+ \`\`\`typescript
1383
+ try {
1384
+ const result = await this.db.query(query, params);
1385
+ } catch (error) {
1386
+ if (error instanceof MySQLConnectionError) {
1387
+ throw new Error(\`Database connection error: \${error.message}\`);
1388
+ } else if (error instanceof MySQLQueryError) {
1389
+ throw new Error(\`Query error: \${error.message}\`);
1390
+ }
1391
+ throw error;
1392
+ }
1393
+ \`\`\`
1394
+
1395
+ ## Frontend System (web/app.js)
1396
+
1397
+ ### Translation System
1398
+
1399
+ **Basic Usage:**
1400
+ \`\`\`javascript
1401
+ // Translate strings
1402
+ t('Hello World') // Returns translated version or original
1403
+ t('Save changes')
1404
+
1405
+ // Set language
1406
+ setLang('pl') // Switch to Polish
1407
+ setLang('en') // Switch to English
1408
+ getCurrentLanguage() // Get current language code
1409
+ \`\`\`
1410
+
1411
+ **Translation File (web/translations.json):**
1412
+ \`\`\`json
1413
+ {
1414
+ "pl": {
1415
+ "Hello World": "Witaj Świecie",
1416
+ "Save changes": "Zapisz zmiany",
1417
+ "Delete": "Usuń"
1418
+ },
1419
+ "ru": {
1420
+ "Hello World": "Привет мир",
1421
+ "Save changes": "Сохранить изменения"
1422
+ }
1423
+ }
1424
+ \`\`\`
1425
+
1426
+ ### UI Feedback & Notifications
1427
+
1428
+ **Toast Notifications:**
1429
+ \`\`\`javascript
1430
+ showToast('Success message', 'success') // Green toast
1431
+ showToast('Error occurred', 'error') // Red toast
1432
+ showToast('Information', 'info') // Blue toast
1433
+ showToast('Warning', 'warning') // Yellow toast
1434
+ \`\`\`
1435
+
1436
+ **Inline Messages:**
1437
+ \`\`\`javascript
1438
+ showMessage('messageId', 'Success!', 'success')
1439
+ showMessage('errorContainer', 'Validation failed', 'error')
1440
+ \`\`\`
1441
+
1442
+ **Modal Dialogs:**
1443
+ \`\`\`javascript
1444
+ showModal('confirmModal', 'Item saved successfully', 'success')
1445
+ showModal('errorModal', 'Operation failed', 'error')
1446
+ \`\`\`
1447
+
1448
+ ### Navigation & Page Actions
1449
+
1450
+ **Navigation Functions:**
1451
+ \`\`\`javascript
1452
+ navigateBack() // Go back in history or to home
1453
+ redirectTo('/posts') // Safe redirect with validation
1454
+ reloadPage() // Reload with loading indicator
1455
+ refreshSection('#content') // Refresh specific section
1456
+
1457
+ // SPA-style navigation
1458
+ navigateToPage('/posts/123') // Loads via AJAX, updates #main
1459
+ \`\`\`
1460
+
1461
+ **Content Management:**
1462
+ \`\`\`javascript
1463
+ updateContent('#results', newHtml, 'replace') // Replace content
1464
+ updateContent('#list', itemHtml, 'append') // Add to end
1465
+ updateContent('#list', itemHtml, 'prepend') // Add to beginning
1466
+ removeElement('#item-123') // Animate and remove
1467
+ \`\`\`
1468
+
1469
+ ### Form Handling & Strategy System
1470
+
1471
+ **Form Strategy Configuration:**
1472
+ \`\`\`html
1473
+ <!-- Form with strategy attributes -->
1474
+ <form data-strategy='["toast", "back"]'
1475
+ data-entity-name="Post"
1476
+ data-field-types='{"age": "number", "active": "boolean"}'>
1477
+ <input name="title" type="text" required>
1478
+ <input name="age" type="number">
1479
+ <input name="active" type="checkbox">
1480
+ <button type="submit">Save</button>
1481
+ </form>
1482
+ \`\`\`
1483
+
1484
+ **Available Strategies:**
1485
+ - \`toast\` - Show success toast notification
1486
+ - \`back\` - Navigate back using browser history
1487
+ - \`message\` - Show inline message in specific element
1488
+ - \`modal\` - Show modal dialog
1489
+ - \`redirect\` - Redirect to specific URL
1490
+ - \`refresh\` - Reload the page
1491
+ - \`remove\` - Remove form element
1492
+
1493
+ **Manual Form Submission:**
1494
+ \`\`\`javascript
1495
+ const form = document.querySelector('#myForm');
1496
+ submitForm(form, ['toast', 'back'], {
1497
+ entityName: 'Post',
1498
+ basePath: '/posts',
1499
+ messageId: 'form-message'
1500
+ });
1501
+ \`\`\`
1502
+
1503
+ **Success Handling:**
1504
+ \`\`\`javascript
1505
+ handleFormSuccess(response, ['toast', 'back'], {
1506
+ entityName: 'Post',
1507
+ basePath: '/posts',
1508
+ messageId: 'success-msg',
1509
+ modalId: 'success-modal'
1510
+ });
1511
+ \`\`\`
1512
+
1513
+ ### Form Validation & Type Conversion
1514
+
1515
+ **Client-side Validation Setup:**
1516
+ \`\`\`javascript
1517
+ setupFormValidation('#createForm'); // Adds required field validation
1518
+ \`\`\`
1519
+
1520
+ **Field Type Conversion:**
1521
+ \`\`\`javascript
1522
+ // Automatic conversion based on data-field-types
1523
+ convertFieldValue('123', 'number') // Returns 123 (number)
1524
+ convertFieldValue('true', 'boolean') // Returns true (boolean)
1525
+ convertFieldValue('text', 'string') // Returns 'text' (string)
1526
+ \`\`\`
1527
+
1528
+ ### Loading States & Utilities
1529
+
1530
+ **Loading Indicators:**
1531
+ \`\`\`javascript
1532
+ showLoading('#form') // Show spinner on form
1533
+ hideLoading('#form') // Hide spinner
1534
+ showLoading('#main') // Show spinner on main content
1535
+ \`\`\`
1536
+
1537
+ **Utility Functions:**
1538
+ \`\`\`javascript
1539
+ debounce(searchFunction, 300) // Debounce for search inputs
1540
+ getElementSafely('#selector') // Safe element selection
1541
+ clearForm('#myForm') // Reset form and clear validation
1542
+ \`\`\`
1543
+
1544
+ ### Event Handling & SPA Integration
1545
+
1546
+ **Automatic Link Handling:**
1547
+ - Internal links automatically use AJAX navigation
1548
+ - External links work normally
1549
+ - Links with \`data-external\` skip AJAX handling
1550
+
1551
+ **Automatic Form Handling:**
1552
+ - Forms with \`data-strategy\` use AJAX submission
1553
+ - Regular forms work normally
1554
+ - Automatic JSON conversion from FormData
1555
+
1556
+ **Custom Event Listeners:**
1557
+ \`\`\`javascript
1558
+ // Re-initialize after dynamic content loading
1559
+ initializeEventListeners();
1560
+
1561
+ // Handle specific link navigation
1562
+ document.querySelector('#myLink').addEventListener('click', (e) => {
1563
+ e.preventDefault();
1564
+ navigateToPage('/custom/path');
1565
+ });
1566
+ \`\`\`
1567
+
1568
+ ### Global App Object
1569
+
1570
+ **Accessing Functions:**
1571
+ \`\`\`javascript
1572
+ // All functions available under window.App
1573
+ App.showToast('Message', 'success');
1574
+ App.navigateBack();
1575
+ App.t('Translate this');
1576
+ App.setLang('pl');
1577
+ App.showLoading('#content');
1578
+ \`\`\`
1579
+
1580
+ ### Template Data Binding
1581
+ \`\`\`html
1582
+ <!-- List with pagination -->
1583
+ <tbody x-for="modules" x-row="module">
1584
+ <tr>
1585
+ <td>{{ module.name }}</td>
1586
+ <td><a href="/module/{{ module.id }}">View</a></td>
1587
+ </tr>
1588
+ </tbody>
1589
+
1590
+ <!-- Form with validation errors -->
1591
+ <div x-if="errors.name" class="text-danger">{{ errors.name }}</div>
1592
+ <input type="text" name="name" value="{{ formData.name || '' }}" class="form-control">
1593
+
1594
+ <!-- Form with strategy attributes -->
1595
+ <form data-strategy='["toast", "back"]'
1596
+ data-entity-name="Module"
1597
+ data-field-types='{"count": "number", "active": "boolean"}'>
1598
+ <!-- form fields -->
1599
+ </form>
1600
+ \`\`\`
1601
+ `;
1602
+ // Directory structure constants
1603
+ exports.DEFAULT_DIRECTORIES = {
1604
+ SRC: 'src',
1605
+ DIST: 'build',
1606
+ WEB: 'web',
1607
+ TEMPLATES: path.join('src', 'common', 'ui', 'templates'),
1608
+ SERVICES: path.join('src', 'common', 'services')
1609
+ };
1610
+ // File names constants
1611
+ exports.DEFAULT_FILES = {
1612
+ PACKAGE_JSON: 'package.json',
1613
+ TSCONFIG: 'tsconfig.json',
1614
+ APP_YAML: 'app.yaml',
1615
+ APP_TS: 'app.ts',
1616
+ MAIN_VIEW: 'main_view.html',
1617
+ ERROR_TEMPLATE: 'error.html',
1618
+ FRONTEND_SCRIPT: 'app.js',
1619
+ TRANSLATIONS: 'translations.json',
1620
+ CURSOR_RULES: '.cursorrules'
1621
+ };