webspresso 0.0.90 → 0.0.93

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 (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
@@ -115,6 +115,24 @@ function readAndParse(absPath, isDevLike = false) {
115
115
  * @param {boolean} isDev NODE_ENV !== 'production' style
116
116
  */
117
117
  function loadNjkRouteTemplate(absPath, isDev) {
118
+ if (!isDev) {
119
+ const cached = prodRouteCache.get(absPath);
120
+ if (cached) {
121
+ return cached;
122
+ }
123
+ if (!fs.existsSync(absPath)) {
124
+ const empty = {
125
+ useStringRender: false,
126
+ templateBody: null,
127
+ metaPatch: {},
128
+ dataPatch: {},
129
+ };
130
+ prodRouteCache.set(absPath, empty);
131
+ return empty;
132
+ }
133
+ return readAndParse(absPath, false);
134
+ }
135
+
118
136
  if (!fs.existsSync(absPath)) {
119
137
  return {
120
138
  useStringRender: false,
@@ -125,21 +143,13 @@ function loadNjkRouteTemplate(absPath, isDev) {
125
143
  }
126
144
 
127
145
  const stat = fs.statSync(absPath);
128
-
129
- if (isDev) {
130
- const prev = devRouteCache.get(absPath);
131
- if (prev && prev.mtimeMs >= stat.mtimeMs) {
132
- return prev.cached;
133
- }
134
- const fresh = readAndParse(absPath, true);
135
- devRouteCache.set(absPath, { mtimeMs: stat.mtimeMs, cached: fresh });
136
- return fresh;
137
- }
138
-
139
- if (prodRouteCache.has(absPath)) {
140
- return prodRouteCache.get(absPath);
146
+ const prev = devRouteCache.get(absPath);
147
+ if (prev && prev.mtimeMs >= stat.mtimeMs) {
148
+ return prev.cached;
141
149
  }
142
- return readAndParse(absPath, false);
150
+ const fresh = readAndParse(absPath, true);
151
+ devRouteCache.set(absPath, { mtimeMs: stat.mtimeMs, cached: fresh });
152
+ return fresh;
143
153
  }
144
154
 
145
155
  function clearNjkFrontmatterCaches() {
@@ -123,20 +123,37 @@ class PluginManager {
123
123
  this.routes = []; // Collected route metadata
124
124
  this.customRoutes = []; // Routes added by plugins
125
125
  this.cspDirectives = new Map(); // directive -> Set of sources (from plugins)
126
+ this.disposers = []; // Array<{ name: string, fn: Function }>
126
127
  this.app = null;
127
128
  this.nunjucksEnv = null;
129
+ this.shutdownManager = null;
130
+ }
131
+
132
+ /**
133
+ * Register a plugin disposer
134
+ * @param {string} name
135
+ * @param {Function} fn
136
+ */
137
+ registerDisposer(name, fn) {
138
+ if (typeof fn === 'function') {
139
+ this.disposers.push({ name: name || 'plugin', fn });
140
+ if (this.shutdownManager && typeof this.shutdownManager.registerDisposer === 'function') {
141
+ this.shutdownManager.registerDisposer(name, fn);
142
+ }
143
+ }
128
144
  }
129
145
 
130
146
  /**
131
147
  * Register plugins with the manager (async version)
132
148
  * @param {Array} plugins - Array of plugin definitions or factory functions
133
- * @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares? }`
149
+ * @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares?, shutdownManager? }`
134
150
  */
135
151
  async register(plugins, context) {
136
152
  if (!plugins || !Array.isArray(plugins)) return;
137
153
 
138
154
  this.app = context.app;
139
155
  this.nunjucksEnv = context.nunjucksEnv;
156
+ this.shutdownManager = context.shutdownManager || null;
140
157
 
141
158
  // Normalize plugins (handle factory functions)
142
159
  const normalizedPlugins = plugins.map(p => {
@@ -159,13 +176,14 @@ class PluginManager {
159
176
  /**
160
177
  * Register plugins with the manager (sync version)
161
178
  * @param {Array} plugins - Array of plugin definitions or factory functions
162
- * @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares? }`
179
+ * @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares?, shutdownManager? }`
163
180
  */
164
181
  registerSync(plugins, context) {
165
182
  if (!plugins || !Array.isArray(plugins)) return;
166
183
 
167
184
  this.app = context.app;
168
185
  this.nunjucksEnv = context.nunjucksEnv;
186
+ this.shutdownManager = context.shutdownManager || null;
169
187
 
170
188
  // Normalize plugins (handle factory functions)
171
189
  const normalizedPlugins = plugins.map(p => {
@@ -276,10 +294,32 @@ class PluginManager {
276
294
  this._collectCspDirectives(plugin.csp);
277
295
  }
278
296
 
297
+ let hasDisposer = false;
298
+
299
+ // Call setup hook if provided
300
+ if (typeof plugin.setup === 'function') {
301
+ try {
302
+ const result = await plugin.setup(ctx.app || ctx);
303
+ if (typeof result === 'function') {
304
+ this.registerDisposer(plugin.name, result);
305
+ hasDisposer = true;
306
+ }
307
+ } catch (err) {
308
+ console.warn(`[plugin-manager] Plugin "${plugin.name}" setup() failed:`, err.message);
309
+ this.plugins.delete(plugin.name);
310
+ if (plugin.api) this.pluginAPIs.delete(plugin.name);
311
+ return;
312
+ }
313
+ }
314
+
279
315
  // Call register hook
280
316
  if (typeof plugin.register === 'function') {
281
317
  try {
282
- await plugin.register(ctx);
318
+ const result = await plugin.register(ctx);
319
+ if (typeof result === 'function') {
320
+ this.registerDisposer(plugin.name, result);
321
+ hasDisposer = true;
322
+ }
283
323
  } catch (err) {
284
324
  console.warn(`[plugin-manager] Plugin "${plugin.name}" register() failed:`, err.message);
285
325
  this.plugins.delete(plugin.name);
@@ -288,6 +328,15 @@ class PluginManager {
288
328
  }
289
329
  }
290
330
 
331
+ // Support explicit shutdown or dispose methods if not already registered
332
+ if (!hasDisposer) {
333
+ if (typeof plugin.shutdown === 'function') {
334
+ this.registerDisposer(plugin.name, () => plugin.shutdown(ctx.app || ctx));
335
+ } else if (typeof plugin.dispose === 'function') {
336
+ this.registerDisposer(plugin.name, () => plugin.dispose(ctx));
337
+ }
338
+ }
339
+
291
340
  // Apply registered helpers to nunjucks
292
341
  this._applyHelpersAndFilters();
293
342
  }
@@ -320,10 +369,32 @@ class PluginManager {
320
369
  this._collectCspDirectives(plugin.csp);
321
370
  }
322
371
 
372
+ let hasDisposer = false;
373
+
374
+ // Call setup hook if provided
375
+ if (typeof plugin.setup === 'function') {
376
+ try {
377
+ const result = plugin.setup(ctx.app || ctx);
378
+ if (typeof result === 'function') {
379
+ this.registerDisposer(plugin.name, result);
380
+ hasDisposer = true;
381
+ }
382
+ } catch (err) {
383
+ console.warn(`[plugin-manager] Plugin "${plugin.name}" setup() failed:`, err.message);
384
+ this.plugins.delete(plugin.name);
385
+ if (plugin.api) this.pluginAPIs.delete(plugin.name);
386
+ return;
387
+ }
388
+ }
389
+
323
390
  // Call register hook (sync - if plugin has async register, it won't wait)
324
391
  if (typeof plugin.register === 'function') {
325
392
  try {
326
- plugin.register(ctx);
393
+ const result = plugin.register(ctx);
394
+ if (typeof result === 'function') {
395
+ this.registerDisposer(plugin.name, result);
396
+ hasDisposer = true;
397
+ }
327
398
  } catch (err) {
328
399
  console.warn(`[plugin-manager] Plugin "${plugin.name}" register() failed:`, err.message);
329
400
  this.plugins.delete(plugin.name);
@@ -332,6 +403,15 @@ class PluginManager {
332
403
  }
333
404
  }
334
405
 
406
+ // Support explicit shutdown or dispose methods if not already registered
407
+ if (!hasDisposer) {
408
+ if (typeof plugin.shutdown === 'function') {
409
+ this.registerDisposer(plugin.name, () => plugin.shutdown(ctx.app || ctx));
410
+ } else if (typeof plugin.dispose === 'function') {
411
+ this.registerDisposer(plugin.name, () => plugin.dispose(ctx));
412
+ }
413
+ }
414
+
335
415
  // Apply registered helpers to nunjucks
336
416
  this._applyHelpersAndFilters();
337
417
  }
@@ -421,6 +501,22 @@ class PluginManager {
421
501
  // Script Injection API
422
502
  // ============================================
423
503
 
504
+ /**
505
+ * Register a cleanup / disposer hook for this plugin
506
+ * @param {Function} fn
507
+ */
508
+ onDispose(fn) {
509
+ self.registerDisposer(plugin.name, fn);
510
+ },
511
+
512
+ /**
513
+ * Alias for onDispose
514
+ * @param {Function} fn
515
+ */
516
+ onShutdown(fn) {
517
+ self.registerDisposer(plugin.name, fn);
518
+ },
519
+
424
520
  /**
425
521
  * Inject content into head section
426
522
  * @param {string} content - HTML/script content
@@ -583,6 +679,30 @@ class PluginManager {
583
679
  return this.plugins.has(name);
584
680
  }
585
681
 
682
+ /**
683
+ * Get all registered disposers
684
+ * @returns {Array<{ name: string, fn: Function }>}
685
+ */
686
+ getDisposers() {
687
+ return this.disposers.slice();
688
+ }
689
+
690
+ /**
691
+ * Execute all registered plugin disposers in reverse registration order
692
+ * @returns {Promise<void>}
693
+ */
694
+ async dispose() {
695
+ const reverseDisposers = this.disposers.slice().reverse();
696
+ for (const { name, fn } of reverseDisposers) {
697
+ try {
698
+ await fn();
699
+ } catch (err) {
700
+ console.warn(`[plugin-manager] Plugin "${name}" disposer failed:`, err.message);
701
+ }
702
+ }
703
+ this.disposers = [];
704
+ }
705
+
586
706
  /**
587
707
  * Get all registered plugin names
588
708
  */