webspresso 0.0.97 → 0.0.98

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 (100) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/bin/commands/add-esbuild.js +80 -0
  7. package/bin/commands/add-tailwind.js +76 -31
  8. package/bin/commands/analytics-prune.js +57 -0
  9. package/bin/commands/api.js +207 -15
  10. package/bin/commands/build.js +99 -0
  11. package/bin/commands/dev.js +64 -35
  12. package/bin/commands/doctor.js +26 -0
  13. package/bin/commands/email-prune.js +47 -0
  14. package/bin/commands/make-job.js +86 -0
  15. package/bin/commands/make-model.js +250 -0
  16. package/bin/commands/new.js +45 -28
  17. package/bin/commands/openapi.js +102 -0
  18. package/bin/commands/page.js +6 -3
  19. package/bin/commands/queue-prune.js +59 -0
  20. package/bin/commands/routes.js +52 -0
  21. package/bin/commands/service.js +6 -3
  22. package/bin/commands/typegen.js +106 -0
  23. package/bin/commands/upload-prune.js +79 -0
  24. package/bin/webspresso.js +22 -0
  25. package/core/applySchema.js +13 -6
  26. package/core/auth/hash.js +21 -12
  27. package/core/compression/middleware.js +23 -17
  28. package/core/openapi/build-from-api-routes.js +160 -29
  29. package/core/orm/cache/fingerprint.js +15 -1
  30. package/core/orm/cache/memory-provider.js +27 -2
  31. package/core/orm/index.js +12 -7
  32. package/core/orm/model.js +49 -7
  33. package/core/queue/adapters/database.js +94 -0
  34. package/core/queue/adapters/memory.js +62 -0
  35. package/core/queue/adapters/redis.js +80 -0
  36. package/core/queue/manager.js +123 -0
  37. package/core/ssr/stream.js +11 -0
  38. package/index.d.ts +660 -10
  39. package/index.js +44 -96
  40. package/package.json +1 -1
  41. package/plugins/admin-panel/api.js +279 -17
  42. package/plugins/admin-panel/app.js +114 -22
  43. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  44. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  46. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  47. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  48. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  49. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  50. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  51. package/plugins/admin-panel/core/admin-module.js +13 -7
  52. package/plugins/admin-panel/core/api-extensions.js +30 -2
  53. package/plugins/admin-panel/core/registry.js +96 -16
  54. package/plugins/admin-panel/index.js +96 -0
  55. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  56. package/plugins/admin-panel/modules/admin-users.js +16 -1
  57. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  58. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  59. package/plugins/admin-panel/modules/dashboard.js +283 -0
  60. package/plugins/admin-panel/modules/menu.js +56 -18
  61. package/plugins/admin-panel/modules/user-management.js +24 -6
  62. package/plugins/audit-log/admin-component.js +11 -0
  63. package/plugins/audit-log/api-handlers.js +34 -0
  64. package/plugins/audit-log/index.js +129 -3
  65. package/plugins/audit-log/purge.js +60 -10
  66. package/plugins/dashboard/app.js +152 -27
  67. package/plugins/dashboard/index.js +143 -39
  68. package/plugins/dashboard/styles.js +78 -0
  69. package/plugins/data-exchange/export-xlsx.js +9 -2
  70. package/plugins/data-exchange/import.js +9 -2
  71. package/plugins/data-exchange/index.js +6 -3
  72. package/plugins/email/index.js +132 -2
  73. package/plugins/email/purge.js +60 -10
  74. package/plugins/index.js +4 -0
  75. package/plugins/orm-cache-admin/index.js +4 -2
  76. package/plugins/queue/index.js +11 -0
  77. package/plugins/schema-explorer/admin-component.js +714 -0
  78. package/plugins/schema-explorer/relations-graph.js +205 -0
  79. package/plugins/schema-explorer.js +248 -69
  80. package/plugins/site-analytics/admin-component.js +23 -0
  81. package/plugins/site-analytics/api-handlers.js +42 -0
  82. package/plugins/site-analytics/index.js +147 -2
  83. package/plugins/site-analytics/purge.js +168 -0
  84. package/plugins/swagger.js +261 -13
  85. package/plugins/upload/index.js +26 -0
  86. package/plugins/upload/orphan-pruner.js +292 -0
  87. package/src/api/api-loader.js +3 -1
  88. package/src/bundle/index.js +517 -0
  89. package/src/bundle/manifest.js +165 -0
  90. package/src/dev/banner.js +296 -0
  91. package/src/dev/colors.js +70 -0
  92. package/src/dev/index.js +62 -0
  93. package/src/dev/profiler.js +354 -0
  94. package/src/dev/typegen.js +497 -0
  95. package/src/dev-reload/index.js +611 -0
  96. package/src/file-router.js +210 -62
  97. package/src/helpers.js +958 -98
  98. package/src/njk-frontmatter.js +12 -0
  99. package/src/pages/page-loader.js +61 -11
  100. package/src/server.js +629 -63
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Development Startup Banner & Route Table Formatter
5
+ * Beautiful terminal dashboard with zero external dependencies
6
+ * @module src/dev/banner
7
+ */
8
+
9
+ const { colors: defaultColors, stripAnsi } = require('./colors');
10
+
11
+ let pkgVersion = '1.0.0';
12
+ try {
13
+ const pkg = require('../../package.json');
14
+ if (pkg && pkg.version) pkgVersion = pkg.version;
15
+ } catch (_) {}
16
+
17
+ /**
18
+ * Formats a clean, readable ASCII table of registered application routes
19
+ * @param {import('../routing/route-table').RouteTable|Array} routeTable
20
+ * @param {Object} [options]
21
+ * @param {boolean} [options.colors=true]
22
+ * @param {number} [options.limit=100]
23
+ * @returns {string} Formatted route table
24
+ */
25
+ function formatRouteTable(routeTable, options = {}) {
26
+ const useColors = options.colors !== false;
27
+ const c = useColors ? defaultColors : {
28
+ cyan: (s) => s,
29
+ green: (s) => s,
30
+ yellow: (s) => s,
31
+ red: (s) => s,
32
+ magenta: (s) => s,
33
+ white: (s) => s,
34
+ gray: (s) => s,
35
+ bold: (s) => s,
36
+ dim: (s) => s,
37
+ };
38
+
39
+ const rawRoutes = Array.isArray(routeTable)
40
+ ? routeTable
41
+ : (routeTable && typeof routeTable.list === 'function'
42
+ ? routeTable.list()
43
+ : (routeTable && routeTable.routes ? routeTable.routes : []));
44
+
45
+ if (!rawRoutes || rawRoutes.length === 0) {
46
+ return c.gray(' (no routes registered)');
47
+ }
48
+
49
+ // Sort: API first or alphabetical by path
50
+ const sorted = [...rawRoutes].sort((a, b) => {
51
+ const pa = a.path || '';
52
+ const pb = b.path || '';
53
+ return pa.localeCompare(pb) || (a.method || '').localeCompare(b.method || '');
54
+ });
55
+
56
+ const limit = options.limit || 100;
57
+ const visible = sorted.slice(0, limit);
58
+
59
+ const lines = [];
60
+ lines.push(` ${c.bold('🛣️ Discovered Routes')} ${c.gray(`(${sorted.length} total)`)}:`);
61
+
62
+ for (const r of visible) {
63
+ const m = (r.method || 'GET').toUpperCase();
64
+ let mCol = m.padEnd(7);
65
+ if (m === 'GET') mCol = c.cyan(mCol);
66
+ else if (m === 'POST') mCol = c.green(mCol);
67
+ else if (m === 'PUT' || m === 'PATCH') mCol = c.yellow(mCol);
68
+ else if (m === 'DELETE') mCol = c.red(mCol);
69
+ else mCol = c.white(mCol);
70
+
71
+ const p = (r.path || '/').padEnd(28);
72
+ const typeLabel = r.type === 'api' ? c.magenta('[API] ') : c.gray('[SSR] ');
73
+ const source = r.source || r.file || '';
74
+ const cleanSource = source ? c.dim(`→ ${source.replace(/\\/g, '/')}`) : '';
75
+
76
+ lines.push(` ${mCol} ${c.bold(p)} ${typeLabel} ${cleanSource}`);
77
+ }
78
+
79
+ if (sorted.length > limit) {
80
+ lines.push(c.gray(` ... and ${sorted.length - limit} more routes (use webspresso routes to list all)`));
81
+ }
82
+
83
+ return lines.join('\n');
84
+ }
85
+
86
+ /**
87
+ * Gathers system and application summary metrics for banner display
88
+ * @param {Object} context
89
+ * @returns {Object}
90
+ */
91
+ function collectBannerSummary(context = {}) {
92
+ const {
93
+ routeTable,
94
+ db,
95
+ serviceRegistry,
96
+ pluginManager,
97
+ port = 3000,
98
+ host = 'localhost',
99
+ } = context;
100
+
101
+ // 1. Routes summary
102
+ const routes = Array.isArray(routeTable)
103
+ ? routeTable
104
+ : (routeTable && typeof routeTable.list === 'function' ? routeTable.list() : (routeTable?.routes || []));
105
+ const apiRoutes = routes.filter((r) => r.type === 'api' || (r.path && r.path.startsWith('/api')));
106
+ const pageRoutes = routes.filter((r) => r.type === 'page' || !apiRoutes.includes(r));
107
+
108
+ // 2. Database client
109
+ let dbDialect = 'None';
110
+ if (db && db.knex) {
111
+ const client = db.knex.client;
112
+ const clientName = client?.config?.client || client?.dialect || 'Connected';
113
+ dbDialect = String(clientName);
114
+ if (dbDialect === 'better-sqlite3' || dbDialect === 'sqlite3') {
115
+ dbDialect = 'SQLite (' + dbDialect + ')';
116
+ } else if (dbDialect === 'pg') {
117
+ dbDialect = 'PostgreSQL';
118
+ } else if (dbDialect === 'mysql' || dbDialect === 'mysql2') {
119
+ dbDialect = 'MySQL';
120
+ }
121
+ }
122
+
123
+ // 3. Models
124
+ let modelsList = [];
125
+ if (db && typeof db.getAllModels === 'function') {
126
+ try {
127
+ modelsList = db.getAllModels().map((m) => (typeof m === 'object' ? m.name : m));
128
+ } catch (_) {}
129
+ }
130
+
131
+ // 4. Services
132
+ let servicesList = [];
133
+ if (serviceRegistry) {
134
+ if (serviceRegistry.services instanceof Map) {
135
+ servicesList = Array.from(serviceRegistry.services.keys());
136
+ } else if (typeof serviceRegistry.list === 'function') {
137
+ servicesList = serviceRegistry.list().map((s) => (typeof s === 'object' ? s.name : s));
138
+ }
139
+ }
140
+
141
+ // 5. Plugins
142
+ let pluginsList = [];
143
+ let hasAdmin = false;
144
+ if (pluginManager && pluginManager.plugins instanceof Map) {
145
+ pluginsList = Array.from(pluginManager.plugins.keys());
146
+ hasAdmin = pluginManager.plugins.has('adminPanel') || pluginManager.plugins.has('admin');
147
+ }
148
+
149
+ // URLs
150
+ const baseUrl = `http://${host}:${port}`;
151
+ const adminUrl = hasAdmin ? `${baseUrl}/_admin` : null;
152
+
153
+ return {
154
+ version: pkgVersion,
155
+ nodeVersion: process.version,
156
+ port,
157
+ host,
158
+ urls: {
159
+ local: baseUrl,
160
+ admin: adminUrl,
161
+ },
162
+ counts: {
163
+ routesTotal: routes.length,
164
+ apiRoutes: apiRoutes.length,
165
+ pageRoutes: pageRoutes.length,
166
+ models: modelsList.length,
167
+ services: servicesList.length,
168
+ plugins: pluginsList.length,
169
+ },
170
+ dbDialect,
171
+ models: modelsList,
172
+ services: servicesList,
173
+ plugins: pluginsList,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Formats the full development startup banner with frame, URLs, and summaries
179
+ * @param {Object} context
180
+ * @param {Object} [options]
181
+ * @returns {{ bannerText: string, plainText: string, summary: Object }}
182
+ */
183
+ function formatDevBanner(context = {}, options = {}) {
184
+ const useColors = options.colors !== false;
185
+ const c = useColors ? defaultColors : {
186
+ cyan: (s) => s,
187
+ green: (s) => s,
188
+ yellow: (s) => s,
189
+ red: (s) => s,
190
+ magenta: (s) => s,
191
+ white: (s) => s,
192
+ gray: (s) => s,
193
+ bold: (s) => s,
194
+ dim: (s) => s,
195
+ };
196
+
197
+ const summary = collectBannerSummary(context);
198
+ const showRouteTable = options.showRouteTable ?? false;
199
+
200
+ const width = 64;
201
+ const topBorder = '┌' + '─'.repeat(width) + '┐';
202
+ const bottomBorder = '└' + '─'.repeat(width) + '┘';
203
+
204
+ function padLine(content, rawLength) {
205
+ const pad = Math.max(0, width - rawLength);
206
+ return '│ ' + content + ' '.repeat(pad > 2 ? pad - 2 : 0) + '│';
207
+ }
208
+
209
+ const lines = [];
210
+ lines.push('');
211
+ lines.push(c.cyan(topBorder));
212
+
213
+ // Header line
214
+ const title = `☕ ${c.bold('Webspresso')} ${c.dim(`v${summary.version}`)}`;
215
+ const nodeTag = c.gray(`Node ${summary.nodeVersion}`);
216
+ const rawHeader = `☕ Webspresso v${summary.version} Node ${summary.nodeVersion}`;
217
+ const spaceCount = Math.max(2, width - 4 - rawHeader.length);
218
+ lines.push('│ ' + title + ' '.repeat(spaceCount) + nodeTag + ' │');
219
+
220
+ lines.push('│' + ' '.repeat(width) + '│');
221
+
222
+ // URLs
223
+ const localStr = `${c.green('➜')} ${c.bold('Local:')} ${c.cyan(summary.urls.local)}`;
224
+ lines.push(padLine(localStr, `➜ Local: ${summary.urls.local}`.length));
225
+
226
+ if (summary.urls.admin) {
227
+ const adminStr = `${c.green('➜')} ${c.bold('Admin:')} ${c.cyan(summary.urls.admin)}`;
228
+ lines.push(padLine(adminStr, `➜ Admin: ${summary.urls.admin}`.length));
229
+ }
230
+
231
+ const dbStr = `${c.gray('•')} ${c.bold('Database:')} ${summary.dbDialect}`;
232
+ lines.push(padLine(dbStr, `• Database: ${summary.dbDialect}`.length));
233
+
234
+ lines.push('│' + ' '.repeat(width) + '│');
235
+
236
+ // Summary header
237
+ const sumHdr = c.bold('📦 Summary:');
238
+ lines.push(padLine(sumHdr, '📦 Summary:'.length));
239
+
240
+ // Route counts
241
+ const routeCountStr = ` ${c.gray('•')} Routes: ${c.bold(String(summary.counts.routesTotal))} ${c.gray(`(${summary.counts.apiRoutes} API, ${summary.counts.pageRoutes} Pages)`)}`;
242
+ lines.push(padLine(routeCountStr, ` • Routes: ${summary.counts.routesTotal} (${summary.counts.apiRoutes} API, ${summary.counts.pageRoutes} Pages)`.length));
243
+
244
+ // Models
245
+ const modelsPreview = summary.models.length > 0 ? ` ${c.gray(`(${summary.models.slice(0, 3).join(', ')}${summary.models.length > 3 ? '...' : ''})`)}` : '';
246
+ const modelsPlain = summary.models.length > 0 ? ` (${summary.models.slice(0, 3).join(', ')}${summary.models.length > 3 ? '...' : ''})` : '';
247
+ const modelStr = ` ${c.gray('•')} Models: ${c.bold(String(summary.counts.models))}${modelsPreview}`;
248
+ lines.push(padLine(modelStr, ` • Models: ${summary.counts.models}${modelsPlain}`.length));
249
+
250
+ // Services
251
+ const servicesPreview = summary.services.length > 0 ? ` ${c.gray(`(${summary.services.slice(0, 3).join(', ')}${summary.services.length > 3 ? '...' : ''})`)}` : '';
252
+ const servicesPlain = summary.services.length > 0 ? ` (${summary.services.slice(0, 3).join(', ')}${summary.services.length > 3 ? '...' : ''})` : '';
253
+ const servStr = ` ${c.gray('•')} Services: ${c.bold(String(summary.counts.services))}${servicesPreview}`;
254
+ lines.push(padLine(servStr, ` • Services: ${summary.counts.services}${servicesPlain}`.length));
255
+
256
+ // Plugins
257
+ const pluginsPreview = summary.plugins.length > 0 ? ` ${c.gray(`(${summary.plugins.slice(0, 4).join(', ')}${summary.plugins.length > 4 ? '...' : ''})`)}` : '';
258
+ const pluginsPlain = summary.plugins.length > 0 ? ` (${summary.plugins.slice(0, 4).join(', ')}${summary.plugins.length > 4 ? '...' : ''})` : '';
259
+ const plugStr = ` ${c.gray('•')} Plugins: ${c.bold(String(summary.counts.plugins))}${pluginsPreview}`;
260
+ lines.push(padLine(plugStr, ` • Plugins: ${summary.counts.plugins}${pluginsPlain}`.length));
261
+
262
+ lines.push(c.cyan(bottomBorder));
263
+ lines.push('');
264
+
265
+ // Route Table append
266
+ if (showRouteTable && context.routeTable) {
267
+ lines.push(formatRouteTable(context.routeTable, { colors: useColors }));
268
+ lines.push('');
269
+ }
270
+
271
+ const bannerText = lines.join('\n');
272
+ const plainText = stripAnsi(bannerText);
273
+
274
+ return { bannerText, plainText, summary };
275
+ }
276
+
277
+ /**
278
+ * Prints the development banner to console or designated logger
279
+ * @param {Object} context
280
+ * @param {Object} [options]
281
+ * @returns {void}
282
+ */
283
+ function printDevBanner(context = {}, options = {}) {
284
+ const logger = options.logger || context.logger || console;
285
+ const { bannerText } = formatDevBanner(context, options);
286
+ if (logger && typeof logger.log === 'function') {
287
+ logger.log(bannerText);
288
+ }
289
+ }
290
+
291
+ module.exports = {
292
+ formatRouteTable,
293
+ collectBannerSummary,
294
+ formatDevBanner,
295
+ printDevBanner,
296
+ };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Dev CLI Colors
5
+ * Lightweight, zero-dependency ANSI color utilities with NO_COLOR & TTY detection
6
+ * @module src/dev/colors
7
+ */
8
+
9
+ const isColorSupported = Boolean(
10
+ !process.env.NO_COLOR &&
11
+ (process.env.FORCE_COLOR || (process.stdout ? process.stdout.isTTY !== false : true))
12
+ );
13
+
14
+ /**
15
+ * Creates color formatter helper functions
16
+ * @param {boolean} [enabled=true]
17
+ */
18
+ function createColors(enabled = isColorSupported) {
19
+ function wrap(open, close) {
20
+ return (str) => {
21
+ if (!enabled || str == null || str === '') return String(str ?? '');
22
+ return `${open}${str}${close}`;
23
+ };
24
+ }
25
+
26
+ return {
27
+ isColorSupported: enabled,
28
+ bold: wrap('\x1b[1m', '\x1b[22m'),
29
+ dim: wrap('\x1b[2m', '\x1b[22m'),
30
+ italic: wrap('\x1b[3m', '\x1b[23m'),
31
+ underline: wrap('\x1b[4m', '\x1b[24m'),
32
+
33
+ // Colors
34
+ black: wrap('\x1b[30m', '\x1b[39m'),
35
+ red: wrap('\x1b[31m', '\x1b[39m'),
36
+ green: wrap('\x1b[32m', '\x1b[39m'),
37
+ yellow: wrap('\x1b[33m', '\x1b[39m'),
38
+ blue: wrap('\x1b[34m', '\x1b[39m'),
39
+ magenta: wrap('\x1b[35m', '\x1b[39m'),
40
+ cyan: wrap('\x1b[36m', '\x1b[39m'),
41
+ white: wrap('\x1b[37m', '\x1b[39m'),
42
+ gray: wrap('\x1b[90m', '\x1b[39m'),
43
+
44
+ // Backgrounds
45
+ bgRed: wrap('\x1b[41m', '\x1b[49m'),
46
+ bgGreen: wrap('\x1b[42m', '\x1b[49m'),
47
+ bgYellow: wrap('\x1b[43m', '\x1b[49m'),
48
+ bgBlue: wrap('\x1b[44m', '\x1b[49m'),
49
+ };
50
+ }
51
+
52
+ const defaultColors = createColors();
53
+
54
+ /**
55
+ * Strip all ANSI escape sequences from a string
56
+ * @param {string} str
57
+ * @returns {string}
58
+ */
59
+ function stripAnsi(str) {
60
+ if (typeof str !== 'string') return '';
61
+ // Match standard ANSI escape codes
62
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
63
+ }
64
+
65
+ module.exports = {
66
+ colors: defaultColors,
67
+ createColors,
68
+ stripAnsi,
69
+ isColorSupported,
70
+ };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Development & DX Tooling
5
+ * Zero-dependency banner, route table introspection, request profiler, and N+1 query detection
6
+ * @module src/dev
7
+ */
8
+
9
+ const { colors, createColors, stripAnsi, isColorSupported } = require('./colors');
10
+ const {
11
+ formatRouteTable,
12
+ collectBannerSummary,
13
+ formatDevBanner,
14
+ printDevBanner,
15
+ } = require('./banner');
16
+ const {
17
+ profilerStorage,
18
+ attachProfilerToKnex,
19
+ extractTableFromSql,
20
+ detectNPlusOne,
21
+ formatRequestLog,
22
+ createDevProfilerMiddleware,
23
+ } = require('./profiler');
24
+
25
+ const {
26
+ zodToTsType,
27
+ collectModelMetadata,
28
+ collectServiceMetadata,
29
+ collectRouteMetadata,
30
+ generateDeclarationCode,
31
+ generateTypes,
32
+ } = require('./typegen');
33
+
34
+ module.exports = {
35
+ // Colors & ANSI
36
+ colors,
37
+ createColors,
38
+ stripAnsi,
39
+ isColorSupported,
40
+
41
+ // Banner & Route Table
42
+ formatRouteTable,
43
+ collectBannerSummary,
44
+ formatDevBanner,
45
+ printDevBanner,
46
+
47
+ // Profiler & N+1 Detection
48
+ profilerStorage,
49
+ attachProfilerToKnex,
50
+ extractTableFromSql,
51
+ detectNPlusOne,
52
+ formatRequestLog,
53
+ createDevProfilerMiddleware,
54
+
55
+ // TypeGen
56
+ zodToTsType,
57
+ collectModelMetadata,
58
+ collectServiceMetadata,
59
+ collectRouteMetadata,
60
+ generateDeclarationCode,
61
+ generateTypes,
62
+ };