webspresso 0.0.100 → 0.0.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +3 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +163 -3
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +7 -0
- package/README.md +18 -5
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/routes.js +8 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/applySchema.js +18 -1
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +10 -0
- package/core/auth/middleware.js +15 -1
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +6 -0
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +4 -0
- package/index.d.ts +422 -1
- package/index.js +111 -16
- package/package.json +9 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +29 -0
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/index.js +13 -0
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +80 -5
- package/src/file-router.js +153 -91
- package/src/helpers.js +143 -0
- package/src/njk-frontmatter.js +17 -1
- package/src/pages/page-loader.js +1 -1
- package/src/plugin-manager.js +12 -0
- package/src/routing/cascading.js +14 -10
- package/src/routing/route-table.js +1 -1
- package/src/server.js +243 -34
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/template/precompile.js +247 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP Compression Middleware
|
|
3
3
|
* Provides threshold-based streaming response compression for Webspresso applications.
|
|
4
|
+
* Backed by the official Express compression package with native Brotli (br), Gzip, and Deflate support.
|
|
4
5
|
* @module core/compression/middleware
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const { isCompressible } = require('./compressible');
|
|
9
|
-
const { createCompressionStream } = require('./compression-stream');
|
|
8
|
+
const compression = require('compression');
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Safely append a field to the HTTP Vary header
|
|
@@ -30,6 +29,15 @@ function appendVary(res, field) {
|
|
|
30
29
|
res.setHeader('Vary', `${currentStr}, ${field}`);
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Response helper to opt-out of compression for a specific route
|
|
34
|
+
* @param {boolean} [enable=true]
|
|
35
|
+
*/
|
|
36
|
+
function resCompress(enable = true) {
|
|
37
|
+
this._noCompression = !enable;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* Create Webspresso HTTP Response Compression Middleware
|
|
35
43
|
* @param {Object} [options]
|
|
@@ -42,271 +50,47 @@ function appendVary(res, field) {
|
|
|
42
50
|
* @param {Object} [options.deflate] - Custom Deflate options
|
|
43
51
|
* @returns {import('express').RequestHandler}
|
|
44
52
|
*/
|
|
45
|
-
function resCompress(enable = true) {
|
|
46
|
-
this._noCompression = !enable;
|
|
47
|
-
return this;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
53
|
function createCompressionMiddleware(options = {}) {
|
|
51
54
|
const threshold = typeof options.threshold === 'number' && options.threshold >= 0 ? options.threshold : 1024;
|
|
52
|
-
const supportedEncodings = Array.isArray(options.encodings) && options.encodings.length > 0
|
|
53
|
-
? options.encodings
|
|
54
|
-
: getDefaultSupportedEncodings();
|
|
55
|
-
|
|
56
55
|
const customFilter = typeof options.filter === 'function' ? options.filter : null;
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (req.method === 'HEAD') {
|
|
65
|
-
return next();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const acceptEncoding = req.headers['accept-encoding'];
|
|
69
|
-
if (!acceptEncoding) {
|
|
70
|
-
return next();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let selectedEncoding = null;
|
|
74
|
-
let checkedEncoding = false;
|
|
75
|
-
|
|
76
|
-
function getSelectedEncoding() {
|
|
77
|
-
if (!checkedEncoding) {
|
|
78
|
-
checkedEncoding = true;
|
|
79
|
-
selectedEncoding = selectEncoding(acceptEncoding, supportedEncodings);
|
|
80
|
-
}
|
|
81
|
-
return selectedEncoding;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Original methods to wrap (store references without .bind() allocation)
|
|
85
|
-
const origWrite = res.write;
|
|
86
|
-
const origEnd = res.end;
|
|
87
|
-
const origWriteHead = res.writeHead;
|
|
88
|
-
|
|
89
|
-
let compressStream = null;
|
|
90
|
-
let isCompressing = false;
|
|
91
|
-
let headersSent = false;
|
|
92
|
-
let bufferedChunks = [];
|
|
93
|
-
let bufferedLength = 0;
|
|
94
|
-
let decided = false;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Check if response should be compressed
|
|
98
|
-
* @param {number} [expectedLength]
|
|
99
|
-
* @returns {boolean}
|
|
100
|
-
*/
|
|
101
|
-
function shouldCompress(expectedLength) {
|
|
57
|
+
const comp = compression({
|
|
58
|
+
threshold,
|
|
59
|
+
brotli: options.brotli,
|
|
60
|
+
level: options.level,
|
|
61
|
+
filter: (req, res) => {
|
|
62
|
+
// Respect Webspresso opt-out flags
|
|
102
63
|
if (res._noCompression || res.locals?.compress === false || res.shouldCompress === false) {
|
|
103
64
|
return false;
|
|
104
65
|
}
|
|
105
|
-
|
|
106
|
-
// Check status codes that must not have a body
|
|
107
|
-
const statusCode = res.statusCode || 200;
|
|
108
|
-
if (statusCode === 204 || statusCode === 304 || statusCode === 205 || statusCode < 200) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// If response already has a Content-Encoding, do not double-compress
|
|
113
|
-
if (res.getHeader('Content-Encoding')) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check Content-Type compressibility
|
|
66
|
+
// Never compress server-sent event streams
|
|
118
67
|
const contentType = res.getHeader('Content-Type');
|
|
119
|
-
if (
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check threshold if length is known
|
|
124
|
-
if (typeof expectedLength === 'number' && expectedLength < threshold) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Custom filter hook
|
|
129
|
-
if (customFilter && !customFilter(req, res)) {
|
|
68
|
+
if (contentType && String(contentType).toLowerCase().includes('text/event-stream')) {
|
|
130
69
|
return false;
|
|
131
70
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const encoding = getSelectedEncoding();
|
|
135
|
-
if (!encoding || encoding === 'identity') {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Initialize compression stream and headers
|
|
144
|
-
*/
|
|
145
|
-
function startCompression() {
|
|
146
|
-
const encoding = getSelectedEncoding();
|
|
147
|
-
compressStream = createCompressionStream(encoding, options);
|
|
148
|
-
if (!compressStream) {
|
|
149
|
-
isCompressing = false;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
isCompressing = true;
|
|
154
|
-
appendVary(res, 'Accept-Encoding');
|
|
155
|
-
res.setHeader('Content-Encoding', encoding);
|
|
156
|
-
res.removeHeader('Content-Length');
|
|
157
|
-
|
|
158
|
-
// Pipe compression stream chunks directly into underlying socket/response
|
|
159
|
-
compressStream.on('data', (chunk) => {
|
|
160
|
-
origWrite.call(res, chunk);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
compressStream.on('end', () => {
|
|
164
|
-
origEnd.call(res);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
compressStream.on('error', (err) => {
|
|
168
|
-
origEnd.call(res);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Wrap writeHead
|
|
173
|
-
res.writeHead = function(statusCode, ...args) {
|
|
174
|
-
if (res.headersSent) return res;
|
|
175
|
-
|
|
176
|
-
if (typeof args[0] === 'object' && args[0] !== null) {
|
|
177
|
-
for (const [k, v] of Object.entries(args[0])) {
|
|
178
|
-
res.setHeader(k, v);
|
|
179
|
-
}
|
|
180
|
-
} else if (typeof args[1] === 'object' && args[1] !== null) {
|
|
181
|
-
for (const [k, v] of Object.entries(args[1])) {
|
|
182
|
-
res.setHeader(k, v);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (isCompressing) {
|
|
187
|
-
const encoding = getSelectedEncoding();
|
|
188
|
-
if (encoding) {
|
|
189
|
-
res.setHeader('Content-Encoding', encoding);
|
|
190
|
-
}
|
|
191
|
-
res.removeHeader('Content-Length');
|
|
192
|
-
appendVary(res, 'Accept-Encoding');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return origWriteHead.call(res, statusCode, ...args);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// Wrap write
|
|
199
|
-
res.write = function(chunk, encoding, callback) {
|
|
200
|
-
if (typeof encoding === 'function') {
|
|
201
|
-
callback = encoding;
|
|
202
|
-
encoding = undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!chunk) {
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
210
|
-
|
|
211
|
-
if (!decided) {
|
|
212
|
-
bufferedChunks.push(buf);
|
|
213
|
-
bufferedLength += buf.length;
|
|
214
|
-
|
|
215
|
-
// If buffered size exceeds threshold, decide to start compression immediately
|
|
216
|
-
if (bufferedLength >= threshold) {
|
|
217
|
-
decided = true;
|
|
218
|
-
if (shouldCompress()) {
|
|
219
|
-
startCompression();
|
|
220
|
-
// Flush buffered chunks through compressor
|
|
221
|
-
for (const b of bufferedChunks) {
|
|
222
|
-
compressStream.write(b);
|
|
223
|
-
}
|
|
224
|
-
bufferedChunks = [];
|
|
225
|
-
} else {
|
|
226
|
-
// Send buffered chunks uncompressed
|
|
227
|
-
for (const b of bufferedChunks) {
|
|
228
|
-
origWrite.call(res, b);
|
|
229
|
-
}
|
|
230
|
-
bufferedChunks = [];
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (typeof callback === 'function') callback();
|
|
234
|
-
return true;
|
|
71
|
+
if (customFilter) {
|
|
72
|
+
return customFilter(req, res);
|
|
235
73
|
}
|
|
74
|
+
return compression.filter(req, res);
|
|
75
|
+
},
|
|
76
|
+
...options,
|
|
77
|
+
});
|
|
236
78
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return origWrite.call(res, buf, callback);
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// Wrap end
|
|
245
|
-
res.end = function(chunk, encoding, callback) {
|
|
246
|
-
if (typeof chunk === 'function') {
|
|
247
|
-
callback = chunk;
|
|
248
|
-
chunk = null;
|
|
249
|
-
} else if (typeof encoding === 'function') {
|
|
250
|
-
callback = encoding;
|
|
251
|
-
encoding = undefined;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (chunk) {
|
|
255
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
256
|
-
if (!decided) {
|
|
257
|
-
bufferedChunks.push(buf);
|
|
258
|
-
bufferedLength += buf.length;
|
|
259
|
-
} else if (isCompressing && compressStream) {
|
|
260
|
-
compressStream.write(buf);
|
|
261
|
-
} else {
|
|
262
|
-
origWrite.call(res, buf);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!decided) {
|
|
267
|
-
decided = true;
|
|
268
|
-
// Check if total length satisfies threshold and compressibility
|
|
269
|
-
if (shouldCompress(bufferedLength)) {
|
|
270
|
-
startCompression();
|
|
271
|
-
if (bufferedChunks.length === 1) {
|
|
272
|
-
if (typeof callback === 'function') {
|
|
273
|
-
compressStream.once('end', callback);
|
|
274
|
-
}
|
|
275
|
-
const singleBuf = bufferedChunks[0];
|
|
276
|
-
bufferedChunks = [];
|
|
277
|
-
compressStream.end(singleBuf);
|
|
278
|
-
return res;
|
|
279
|
-
}
|
|
280
|
-
for (const b of bufferedChunks) {
|
|
281
|
-
compressStream.write(b);
|
|
282
|
-
}
|
|
283
|
-
bufferedChunks = [];
|
|
284
|
-
} else {
|
|
285
|
-
// Output uncompressed
|
|
286
|
-
if (shouldCompress(Infinity) && bufferedLength < threshold) {
|
|
287
|
-
// Ensure Vary is set if content was compressible but just below threshold
|
|
288
|
-
appendVary(res, 'Accept-Encoding');
|
|
289
|
-
}
|
|
290
|
-
for (const b of bufferedChunks) {
|
|
291
|
-
origWrite.call(res, b);
|
|
292
|
-
}
|
|
293
|
-
bufferedChunks = [];
|
|
294
|
-
return origEnd.call(res, callback);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
79
|
+
return function compressionMiddleware(req, res, next) {
|
|
80
|
+
// Expose opt-out API on response object
|
|
81
|
+
res._noCompression = false;
|
|
82
|
+
res.compress = resCompress;
|
|
297
83
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
compressStream.end();
|
|
84
|
+
// Prevent Content-Length header mismatch if setHeader is invoked after compression starts
|
|
85
|
+
const origSetHeader = res.setHeader;
|
|
86
|
+
res.setHeader = function(field, val) {
|
|
87
|
+
if (res.getHeader('Content-Encoding') && typeof field === 'string' && field.toLowerCase() === 'content-length') {
|
|
303
88
|
return res;
|
|
304
89
|
}
|
|
305
|
-
|
|
306
|
-
return origEnd.call(res, callback);
|
|
90
|
+
return origSetHeader.call(res, field, val);
|
|
307
91
|
};
|
|
308
92
|
|
|
309
|
-
next
|
|
93
|
+
return comp(req, res, next);
|
|
310
94
|
};
|
|
311
95
|
}
|
|
312
96
|
|
package/core/content/service.js
CHANGED
|
@@ -81,7 +81,10 @@ function createContentService(db, options = {}) {
|
|
|
81
81
|
const cached = cache.get(key);
|
|
82
82
|
if (cached) return cached;
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
let row = await TypeRepo().findById(id);
|
|
85
|
+
if (!row && typeof id === 'string' && /^\d+$/.test(id)) {
|
|
86
|
+
row = await TypeRepo().findById(parseInt(id, 10));
|
|
87
|
+
}
|
|
85
88
|
cacheTypeRow(row);
|
|
86
89
|
return row;
|
|
87
90
|
}
|
|
@@ -281,7 +284,10 @@ function createContentService(db, options = {}) {
|
|
|
281
284
|
const cached = cache.get(key);
|
|
282
285
|
if (cached) return cached;
|
|
283
286
|
|
|
284
|
-
|
|
287
|
+
let entry = await EntryRepo().findById(id);
|
|
288
|
+
if (!entry && typeof id === 'string' && /^\d+$/.test(id)) {
|
|
289
|
+
entry = await EntryRepo().findById(parseInt(id, 10));
|
|
290
|
+
}
|
|
285
291
|
if (!entry) return null;
|
|
286
292
|
|
|
287
293
|
const type = await getTypeById(entry.content_type_id);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Events - File-based Listener Discovery & Definition
|
|
3
|
+
* Auto-discovers event listeners from `listeners/` or `events/` directories.
|
|
4
|
+
* @module core/events/discovery
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Define a typed event listener
|
|
12
|
+
* @param {string|string[]} event - Event name or array of event names
|
|
13
|
+
* @param {Function} handler - Handler function (data, meta) => any
|
|
14
|
+
* @param {Object} [options] - Listener options ({ sender, priority, once })
|
|
15
|
+
* @returns {Object}
|
|
16
|
+
*/
|
|
17
|
+
function defineListener(event, handler, options = {}) {
|
|
18
|
+
if (!event) {
|
|
19
|
+
throw new TypeError('Listener event must be specified');
|
|
20
|
+
}
|
|
21
|
+
if (typeof handler !== 'function') {
|
|
22
|
+
throw new TypeError('Listener handler must be a function');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
event,
|
|
26
|
+
handle: handler,
|
|
27
|
+
...options,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert file path to dot-notation event name fallback
|
|
33
|
+
* e.g. 'order/created.js' -> 'order.created'
|
|
34
|
+
* @param {string} relPath
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function filePathToEventName(relPath) {
|
|
38
|
+
const parsed = path.parse(relPath);
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (parsed.dir) {
|
|
41
|
+
parts.push(...parsed.dir.split(path.sep));
|
|
42
|
+
}
|
|
43
|
+
parts.push(parsed.name);
|
|
44
|
+
return parts.join('.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recursively find all .js / .cjs files in a directory
|
|
49
|
+
* @param {string} dir
|
|
50
|
+
* @returns {string[]}
|
|
51
|
+
*/
|
|
52
|
+
function walkDirectory(dir) {
|
|
53
|
+
const files = [];
|
|
54
|
+
if (!fs.existsSync(dir)) return files;
|
|
55
|
+
|
|
56
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const fullPath = path.join(dir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
files.push(...walkDirectory(fullPath));
|
|
61
|
+
} else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
|
|
62
|
+
if (!entry.name.startsWith('_') && !entry.name.endsWith('.test.js')) {
|
|
63
|
+
files.push(fullPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load and register listeners from a directory into an EventDispatcher
|
|
72
|
+
* @param {string} dirPath - Absolute or relative directory path
|
|
73
|
+
* @param {import('./dispatcher').EventDispatcher} dispatcher
|
|
74
|
+
* @param {Object} [options]
|
|
75
|
+
* @returns {Array<{ file: string, event: string, handler: Function }>}
|
|
76
|
+
*/
|
|
77
|
+
function loadListeners(dirPath, dispatcher, options = {}) {
|
|
78
|
+
if (!dispatcher || typeof dispatcher.on !== 'function') {
|
|
79
|
+
throw new TypeError('A valid EventDispatcher instance must be provided');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolvedDir = path.resolve(dirPath);
|
|
83
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filePaths = walkDirectory(resolvedDir);
|
|
88
|
+
const loaded = [];
|
|
89
|
+
|
|
90
|
+
for (const filePath of filePaths) {
|
|
91
|
+
try {
|
|
92
|
+
const moduleContent = require(filePath);
|
|
93
|
+
const candidates = Array.isArray(moduleContent) ? moduleContent : [moduleContent];
|
|
94
|
+
|
|
95
|
+
for (const item of candidates) {
|
|
96
|
+
if (!item) continue;
|
|
97
|
+
|
|
98
|
+
let eventName = item.event;
|
|
99
|
+
let handler = item.handle || item.handler;
|
|
100
|
+
|
|
101
|
+
// If default export is a function directly
|
|
102
|
+
if (typeof item === 'function') {
|
|
103
|
+
handler = item;
|
|
104
|
+
eventName = item.event || filePathToEventName(path.relative(resolvedDir, filePath));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (handler && typeof handler === 'function') {
|
|
108
|
+
if (!eventName) {
|
|
109
|
+
eventName = filePathToEventName(path.relative(resolvedDir, filePath));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const eventsList = Array.isArray(eventName) ? eventName : [eventName];
|
|
113
|
+
for (const evt of eventsList) {
|
|
114
|
+
dispatcher.on(evt, handler, {
|
|
115
|
+
sender: item.sender,
|
|
116
|
+
priority: item.priority,
|
|
117
|
+
once: item.once,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
loaded.push({
|
|
121
|
+
file: filePath,
|
|
122
|
+
event: evt,
|
|
123
|
+
handler,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (options.logger && typeof options.logger.warn === 'function') {
|
|
130
|
+
options.logger.warn(`[loadListeners] Failed to load listener from ${filePath}:`, err.message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return loaded;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
defineListener,
|
|
140
|
+
loadListeners,
|
|
141
|
+
filePathToEventName,
|
|
142
|
+
};
|