@morojs/moro 1.5.15 → 1.5.17
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/dist/core/config/config-sources.js +188 -0
- package/dist/core/config/config-sources.js.map +1 -1
- package/dist/core/framework.d.ts +2 -0
- package/dist/core/framework.js +32 -4
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/http-server.js +2 -147
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/middleware/built-in/auth.js +46 -35
- package/dist/core/middleware/built-in/auth.js.map +1 -1
- package/dist/core/middleware/built-in/jwt-helpers.d.ts +26 -1
- package/dist/core/middleware/built-in/jwt-helpers.js +26 -1
- package/dist/core/middleware/built-in/jwt-helpers.js.map +1 -1
- package/dist/core/modules/auto-discovery.d.ts +4 -0
- package/dist/core/modules/auto-discovery.js +127 -7
- package/dist/core/modules/auto-discovery.js.map +1 -1
- package/dist/moro.d.ts +17 -0
- package/dist/moro.js +134 -28
- package/dist/moro.js.map +1 -1
- package/package.json +2 -3
- package/src/core/config/config-sources.ts +212 -0
- package/src/core/framework.ts +42 -5
- package/src/core/http/http-server.ts +2 -150
- package/src/core/middleware/built-in/auth.ts +60 -37
- package/src/core/middleware/built-in/jwt-helpers.ts +26 -1
- package/src/core/modules/auto-discovery.ts +160 -8
- package/src/moro.ts +163 -28
|
@@ -285,64 +285,6 @@ export class MoroHttpServer {
|
|
|
285
285
|
// Execute handler
|
|
286
286
|
await route.handler(httpReq, httpRes);
|
|
287
287
|
} catch (error) {
|
|
288
|
-
// Handle JWT-specific errors gracefully
|
|
289
|
-
if (
|
|
290
|
-
error instanceof Error &&
|
|
291
|
-
(error.name === 'TokenExpiredError' ||
|
|
292
|
-
error.name === 'JsonWebTokenError' ||
|
|
293
|
-
error.name === 'NotBeforeError')
|
|
294
|
-
) {
|
|
295
|
-
this.logger.debug('JWT authentication error', 'RequestHandler', {
|
|
296
|
-
errorType: error.name,
|
|
297
|
-
errorMessage: error.message,
|
|
298
|
-
requestPath: req.url,
|
|
299
|
-
requestMethod: req.method,
|
|
300
|
-
requestId: httpReq.requestId,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
if (!httpRes.headersSent) {
|
|
304
|
-
if (typeof httpRes.status === 'function' && typeof httpRes.json === 'function') {
|
|
305
|
-
if (error.name === 'TokenExpiredError') {
|
|
306
|
-
httpRes.status(401).json({
|
|
307
|
-
success: false,
|
|
308
|
-
error: 'Token expired',
|
|
309
|
-
message: 'Your session has expired. Please sign in again.',
|
|
310
|
-
requestId: httpReq.requestId,
|
|
311
|
-
expiredAt: (error as any).expiredAt,
|
|
312
|
-
});
|
|
313
|
-
} else if (error.name === 'JsonWebTokenError') {
|
|
314
|
-
httpRes.status(401).json({
|
|
315
|
-
success: false,
|
|
316
|
-
error: 'Invalid token',
|
|
317
|
-
message: 'The provided authentication token is invalid.',
|
|
318
|
-
requestId: httpReq.requestId,
|
|
319
|
-
});
|
|
320
|
-
} else if (error.name === 'NotBeforeError') {
|
|
321
|
-
httpRes.status(401).json({
|
|
322
|
-
success: false,
|
|
323
|
-
error: 'Token not ready',
|
|
324
|
-
message: 'The authentication token is not yet valid.',
|
|
325
|
-
requestId: httpReq.requestId,
|
|
326
|
-
availableAt: (error as any).date,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
// Fallback for non-enhanced response objects
|
|
331
|
-
httpRes.statusCode = 401;
|
|
332
|
-
httpRes.setHeader('Content-Type', 'application/json');
|
|
333
|
-
httpRes.end(
|
|
334
|
-
JSON.stringify({
|
|
335
|
-
success: false,
|
|
336
|
-
error: 'Authentication failed',
|
|
337
|
-
message: 'JWT authentication error occurred.',
|
|
338
|
-
requestId: httpReq.requestId,
|
|
339
|
-
})
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
288
|
// Debug: Log the actual error and where it came from
|
|
347
289
|
this.logger.debug('Request error details', 'RequestHandler', {
|
|
348
290
|
errorType: typeof error,
|
|
@@ -1074,101 +1016,11 @@ export class MoroHttpServer {
|
|
|
1074
1016
|
if (!nextCalled) next();
|
|
1075
1017
|
})
|
|
1076
1018
|
.catch(error => {
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1079
|
-
error instanceof Error &&
|
|
1080
|
-
(error.name === 'TokenExpiredError' ||
|
|
1081
|
-
error.name === 'JsonWebTokenError' ||
|
|
1082
|
-
error.name === 'NotBeforeError')
|
|
1083
|
-
) {
|
|
1084
|
-
if (!res.headersSent) {
|
|
1085
|
-
if (typeof res.status === 'function' && typeof res.json === 'function') {
|
|
1086
|
-
if (error.name === 'TokenExpiredError') {
|
|
1087
|
-
res.status(401).json({
|
|
1088
|
-
success: false,
|
|
1089
|
-
error: 'Token expired',
|
|
1090
|
-
message: 'Your session has expired. Please sign in again.',
|
|
1091
|
-
expiredAt: (error as any).expiredAt,
|
|
1092
|
-
});
|
|
1093
|
-
} else if (error.name === 'JsonWebTokenError') {
|
|
1094
|
-
res.status(401).json({
|
|
1095
|
-
success: false,
|
|
1096
|
-
error: 'Invalid token',
|
|
1097
|
-
message: 'The provided authentication token is invalid.',
|
|
1098
|
-
});
|
|
1099
|
-
} else if (error.name === 'NotBeforeError') {
|
|
1100
|
-
res.status(401).json({
|
|
1101
|
-
success: false,
|
|
1102
|
-
error: 'Token not ready',
|
|
1103
|
-
message: 'The authentication token is not yet valid.',
|
|
1104
|
-
availableAt: (error as any).date,
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
} else {
|
|
1108
|
-
res.statusCode = 401;
|
|
1109
|
-
res.setHeader('Content-Type', 'application/json');
|
|
1110
|
-
res.end(
|
|
1111
|
-
JSON.stringify({
|
|
1112
|
-
success: false,
|
|
1113
|
-
error: 'Authentication failed',
|
|
1114
|
-
message: 'JWT authentication error occurred.',
|
|
1115
|
-
})
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
resolve(); // Continue middleware chain
|
|
1120
|
-
} else {
|
|
1121
|
-
reject(error);
|
|
1122
|
-
}
|
|
1019
|
+
reject(error);
|
|
1123
1020
|
});
|
|
1124
1021
|
}
|
|
1125
1022
|
} catch (error) {
|
|
1126
|
-
|
|
1127
|
-
if (
|
|
1128
|
-
error instanceof Error &&
|
|
1129
|
-
(error.name === 'TokenExpiredError' ||
|
|
1130
|
-
error.name === 'JsonWebTokenError' ||
|
|
1131
|
-
error.name === 'NotBeforeError')
|
|
1132
|
-
) {
|
|
1133
|
-
if (!res.headersSent) {
|
|
1134
|
-
if (typeof res.status === 'function' && typeof res.json === 'function') {
|
|
1135
|
-
if (error.name === 'TokenExpiredError') {
|
|
1136
|
-
res.status(401).json({
|
|
1137
|
-
success: false,
|
|
1138
|
-
error: 'Token expired',
|
|
1139
|
-
message: 'Your session has expired. Please sign in again.',
|
|
1140
|
-
expiredAt: (error as any).expiredAt,
|
|
1141
|
-
});
|
|
1142
|
-
} else if (error.name === 'JsonWebTokenError') {
|
|
1143
|
-
res.status(401).json({
|
|
1144
|
-
success: false,
|
|
1145
|
-
error: 'Invalid token',
|
|
1146
|
-
message: 'The provided authentication token is invalid.',
|
|
1147
|
-
});
|
|
1148
|
-
} else if (error.name === 'NotBeforeError') {
|
|
1149
|
-
res.status(401).json({
|
|
1150
|
-
success: false,
|
|
1151
|
-
error: 'Token not ready',
|
|
1152
|
-
message: 'The authentication token is not yet valid.',
|
|
1153
|
-
availableAt: (error as any).date,
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
} else {
|
|
1157
|
-
res.statusCode = 401;
|
|
1158
|
-
res.setHeader('Content-Type', 'application/json');
|
|
1159
|
-
res.end(
|
|
1160
|
-
JSON.stringify({
|
|
1161
|
-
success: false,
|
|
1162
|
-
error: 'Authentication failed',
|
|
1163
|
-
message: 'JWT authentication error occurred.',
|
|
1164
|
-
})
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
resolve(); // Continue middleware chain
|
|
1169
|
-
} else {
|
|
1170
|
-
reject(error);
|
|
1171
|
-
}
|
|
1023
|
+
reject(error);
|
|
1172
1024
|
}
|
|
1173
1025
|
});
|
|
1174
1026
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CredentialsProvider,
|
|
13
13
|
EmailProvider,
|
|
14
14
|
} from '../../../types/auth';
|
|
15
|
+
import { safeVerifyJWT, createAuthErrorResponse } from './jwt-helpers';
|
|
15
16
|
|
|
16
17
|
const logger = createFrameworkLogger('AuthMiddleware');
|
|
17
18
|
|
|
@@ -186,27 +187,59 @@ export const auth = (options: AuthOptions): MiddlewareInterface => ({
|
|
|
186
187
|
session = await authInstance.getSession({ req: { ...req, token } });
|
|
187
188
|
}
|
|
188
189
|
} catch (error: any) {
|
|
189
|
-
// Handle specific JWT errors gracefully
|
|
190
|
+
// Handle specific JWT errors gracefully and return proper HTTP responses
|
|
190
191
|
if (error.name === 'TokenExpiredError') {
|
|
191
192
|
logger.debug('JWT token expired', 'TokenValidation', {
|
|
192
193
|
message: error.message,
|
|
193
194
|
expiredAt: error.expiredAt,
|
|
194
195
|
});
|
|
196
|
+
|
|
197
|
+
// If this is a protected route request, return a proper 401 response
|
|
198
|
+
if (req.headers.accept?.includes('application/json')) {
|
|
199
|
+
return res.status(401).json(
|
|
200
|
+
createAuthErrorResponse({
|
|
201
|
+
type: 'expired',
|
|
202
|
+
message: error.message,
|
|
203
|
+
expiredAt: error.expiredAt,
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
}
|
|
195
207
|
} else if (error.name === 'JsonWebTokenError') {
|
|
196
208
|
logger.debug('Invalid JWT token format', 'TokenValidation', {
|
|
197
209
|
message: error.message,
|
|
198
210
|
});
|
|
211
|
+
|
|
212
|
+
// If this is a protected route request, return a proper 401 response
|
|
213
|
+
if (req.headers.accept?.includes('application/json')) {
|
|
214
|
+
return res.status(401).json(
|
|
215
|
+
createAuthErrorResponse({
|
|
216
|
+
type: 'invalid',
|
|
217
|
+
message: error.message,
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
199
221
|
} else if (error.name === 'NotBeforeError') {
|
|
200
222
|
logger.debug('JWT token not active yet', 'TokenValidation', {
|
|
201
223
|
message: error.message,
|
|
202
224
|
date: error.date,
|
|
203
225
|
});
|
|
226
|
+
|
|
227
|
+
// If this is a protected route request, return a proper 401 response
|
|
228
|
+
if (req.headers.accept?.includes('application/json')) {
|
|
229
|
+
return res.status(401).json(
|
|
230
|
+
createAuthErrorResponse({
|
|
231
|
+
type: 'malformed',
|
|
232
|
+
message: error.message,
|
|
233
|
+
date: error.date,
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
}
|
|
204
237
|
} else {
|
|
205
238
|
logger.debug('JWT token validation failed', 'TokenValidation', {
|
|
206
239
|
error: error.message || error,
|
|
207
240
|
});
|
|
208
241
|
}
|
|
209
|
-
// Continue with unauthenticated state -
|
|
242
|
+
// Continue with unauthenticated state for non-API requests
|
|
210
243
|
}
|
|
211
244
|
}
|
|
212
245
|
|
|
@@ -298,44 +331,34 @@ async function initializeAuthJS(config: AuthOptions): Promise<any> {
|
|
|
298
331
|
},
|
|
299
332
|
|
|
300
333
|
verifyJWT: async (token: string) => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
334
|
+
const secret = process.env.JWT_SECRET || config.jwt?.secret || config.secret || '';
|
|
335
|
+
|
|
336
|
+
// Use the safe JWT verification function
|
|
337
|
+
const result = safeVerifyJWT(token, secret);
|
|
338
|
+
|
|
339
|
+
if (!result.success) {
|
|
340
|
+
// Create a custom error that includes the structured error information
|
|
341
|
+
const customError = new Error(result.error?.message || 'JWT verification failed');
|
|
342
|
+
|
|
343
|
+
// Add the error type information for upstream handling
|
|
344
|
+
(customError as any).jwtErrorType = result.error?.type;
|
|
345
|
+
(customError as any).jwtErrorDetails = result.error;
|
|
346
|
+
|
|
347
|
+
// Map the safe error types back to standard JWT error names for compatibility
|
|
348
|
+
if (result.error?.type === 'expired') {
|
|
349
|
+
customError.name = 'TokenExpiredError';
|
|
350
|
+
(customError as any).expiredAt = result.error.expiredAt;
|
|
351
|
+
} else if (result.error?.type === 'invalid') {
|
|
352
|
+
customError.name = 'JsonWebTokenError';
|
|
353
|
+
} else if (result.error?.type === 'malformed') {
|
|
354
|
+
customError.name = 'NotBeforeError';
|
|
355
|
+
(customError as any).date = result.error.date;
|
|
356
|
+
}
|
|
311
357
|
|
|
312
|
-
|
|
313
|
-
if (!secret) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
'JWT verification requires a secret. ' +
|
|
316
|
-
'Please set JWT_SECRET environment variable, or provide jwt.secret or secret in auth config.'
|
|
317
|
-
);
|
|
358
|
+
throw customError;
|
|
318
359
|
}
|
|
319
360
|
|
|
320
|
-
|
|
321
|
-
const decoded = jwt.verify(token, secret);
|
|
322
|
-
return decoded;
|
|
323
|
-
} catch (error: any) {
|
|
324
|
-
// Handle specific JWT errors gracefully
|
|
325
|
-
if (error.name === 'TokenExpiredError') {
|
|
326
|
-
// Token expired - handled gracefully by auth middleware
|
|
327
|
-
throw error;
|
|
328
|
-
} else if (error.name === 'JsonWebTokenError') {
|
|
329
|
-
// Invalid token format
|
|
330
|
-
throw error;
|
|
331
|
-
} else if (error.name === 'NotBeforeError') {
|
|
332
|
-
// Token not active yet
|
|
333
|
-
throw error;
|
|
334
|
-
} else {
|
|
335
|
-
// Other JWT errors
|
|
336
|
-
throw new Error(`JWT verification failed: ${error.message}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
361
|
+
return result.payload;
|
|
339
362
|
},
|
|
340
363
|
|
|
341
364
|
signIn: async (provider?: string, options?: any) => {
|
|
@@ -178,7 +178,7 @@ export function createAuthErrorResponse(error: JWTVerificationResult['error']) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
|
-
* Example usage for custom middleware:
|
|
181
|
+
* Example usage for custom middleware with elegant error handling:
|
|
182
182
|
*
|
|
183
183
|
* ```typescript
|
|
184
184
|
* import { safeVerifyJWT, extractJWTFromHeader, createAuthErrorResponse } from '@morojs/moro';
|
|
@@ -197,6 +197,7 @@ export function createAuthErrorResponse(error: JWTVerificationResult['error']) {
|
|
|
197
197
|
* const result = safeVerifyJWT(token, process.env.JWT_SECRET!);
|
|
198
198
|
*
|
|
199
199
|
* if (!result.success) {
|
|
200
|
+
* // This provides elegant, user-friendly error messages instead of stack traces
|
|
200
201
|
* const errorResponse = createAuthErrorResponse(result.error);
|
|
201
202
|
* return res.status(401).json(errorResponse);
|
|
202
203
|
* }
|
|
@@ -212,4 +213,28 @@ export function createAuthErrorResponse(error: JWTVerificationResult['error']) {
|
|
|
212
213
|
* next();
|
|
213
214
|
* };
|
|
214
215
|
* ```
|
|
216
|
+
*
|
|
217
|
+
* Benefits of using safeVerifyJWT vs raw jsonwebtoken.verify():
|
|
218
|
+
*
|
|
219
|
+
* ❌ Raw approach (shows ugly error messages to users):
|
|
220
|
+
* ```typescript
|
|
221
|
+
* try {
|
|
222
|
+
* const decoded = jwt.verify(token, secret);
|
|
223
|
+
* req.user = decoded;
|
|
224
|
+
* } catch (error) {
|
|
225
|
+
* // This exposes technical details and stack traces to users:
|
|
226
|
+
* // "Invalid token: TokenExpiredError: jwt expired at /node_modules/jsonwebtoken/verify.js:190:21..."
|
|
227
|
+
* throw error; // BAD - exposes internal details
|
|
228
|
+
* }
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* ✅ Safe approach (shows clean, user-friendly messages):
|
|
232
|
+
* ```typescript
|
|
233
|
+
* const result = safeVerifyJWT(token, secret);
|
|
234
|
+
* if (!result.success) {
|
|
235
|
+
* // This returns clean messages like:
|
|
236
|
+
* // { "error": "Token expired", "message": "Your session has expired. Please sign in again." }
|
|
237
|
+
* return res.status(401).json(createAuthErrorResponse(result.error));
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
215
240
|
*/
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// Auto-discovery system for Moro modules
|
|
2
2
|
import { readdirSync, statSync } from 'fs';
|
|
3
|
-
import { join, extname, relative } from 'path';
|
|
3
|
+
import { join, extname, relative, isAbsolute } from 'path';
|
|
4
4
|
import { ModuleConfig } from '../../types/module';
|
|
5
5
|
import { DiscoveryOptions } from '../../types/discovery';
|
|
6
6
|
import { ModuleDefaultsConfig } from '../../types/config';
|
|
7
7
|
import { createFrameworkLogger } from '../logger';
|
|
8
|
-
import { minimatch } from 'minimatch';
|
|
9
8
|
|
|
10
9
|
export class ModuleDiscovery {
|
|
11
10
|
private baseDir: string;
|
|
@@ -219,19 +218,32 @@ export class ModuleDiscovery {
|
|
|
219
218
|
const fullPath = join(this.baseDir, searchPath);
|
|
220
219
|
|
|
221
220
|
try {
|
|
222
|
-
|
|
221
|
+
const stat = statSync(fullPath);
|
|
222
|
+
|
|
223
|
+
if (!stat.isDirectory()) {
|
|
223
224
|
return modules;
|
|
224
225
|
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return modules;
|
|
228
|
+
}
|
|
225
229
|
|
|
226
|
-
|
|
230
|
+
try {
|
|
231
|
+
const files = await this.findMatchingFilesWithGlob(
|
|
232
|
+
fullPath,
|
|
233
|
+
config.patterns,
|
|
234
|
+
config.ignorePatterns,
|
|
235
|
+
config.maxDepth
|
|
236
|
+
);
|
|
227
237
|
|
|
228
238
|
for (const filePath of files) {
|
|
229
239
|
try {
|
|
230
|
-
|
|
240
|
+
// Convert relative path to absolute path for import
|
|
241
|
+
const absolutePath = join(this.baseDir, filePath);
|
|
242
|
+
const module = await this.loadModule(absolutePath);
|
|
231
243
|
if (module && this.validateAdvancedModule(module, config)) {
|
|
232
244
|
modules.push(module);
|
|
233
245
|
this.discoveryLogger.info(
|
|
234
|
-
`Auto-discovered module: ${module.name}@${module.version} from ${
|
|
246
|
+
`Auto-discovered module: ${module.name}@${module.version} from ${filePath}`
|
|
235
247
|
);
|
|
236
248
|
}
|
|
237
249
|
} catch (error) {
|
|
@@ -302,14 +314,154 @@ export class ModuleDiscovery {
|
|
|
302
314
|
return files;
|
|
303
315
|
}
|
|
304
316
|
|
|
317
|
+
// Use native Node.js glob to find matching files
|
|
318
|
+
private async findMatchingFilesWithGlob(
|
|
319
|
+
searchPath: string,
|
|
320
|
+
patterns: string[],
|
|
321
|
+
ignorePatterns: string[],
|
|
322
|
+
maxDepth: number = 5
|
|
323
|
+
): Promise<string[]> {
|
|
324
|
+
// Force fallback in CI environments or if Node.js version is uncertain
|
|
325
|
+
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
326
|
+
|
|
327
|
+
if (isCI) {
|
|
328
|
+
return this.findMatchingFilesFallback(searchPath, patterns, ignorePatterns, maxDepth);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const allFiles: string[] = [];
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
// Try to use native fs.glob if available (Node.js 20+)
|
|
335
|
+
const { glob } = await import('fs/promises');
|
|
336
|
+
|
|
337
|
+
// Check if glob is actually a function and test it
|
|
338
|
+
if (typeof glob !== 'function') {
|
|
339
|
+
return this.findMatchingFilesFallback(searchPath, patterns, ignorePatterns, maxDepth);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Test glob with a simple pattern first
|
|
343
|
+
try {
|
|
344
|
+
const testIterator = glob(join(searchPath, '*'));
|
|
345
|
+
let testCount = 0;
|
|
346
|
+
for await (const _ of testIterator) {
|
|
347
|
+
testCount++;
|
|
348
|
+
if (testCount > 0) break; // Just test that it works
|
|
349
|
+
}
|
|
350
|
+
} catch (testError) {
|
|
351
|
+
return this.findMatchingFilesFallback(searchPath, patterns, ignorePatterns, maxDepth);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const pattern of patterns) {
|
|
355
|
+
const fullPattern = join(searchPath, pattern);
|
|
356
|
+
try {
|
|
357
|
+
// fs.glob returns an AsyncIterator, need to collect results
|
|
358
|
+
const globIterator = glob(fullPattern);
|
|
359
|
+
const files: string[] = [];
|
|
360
|
+
|
|
361
|
+
for await (const file of globIterator) {
|
|
362
|
+
const filePath = typeof file === 'string' ? file : (file as any).name || String(file);
|
|
363
|
+
const relativePath = relative(this.baseDir, filePath);
|
|
364
|
+
|
|
365
|
+
// Check if file should be ignored and within max depth
|
|
366
|
+
if (
|
|
367
|
+
!this.shouldIgnore(relativePath, ignorePatterns) &&
|
|
368
|
+
this.isWithinMaxDepth(relativePath, searchPath, maxDepth)
|
|
369
|
+
) {
|
|
370
|
+
files.push(relativePath);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
allFiles.push(...files);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
// If any glob call fails, fall back to manual discovery
|
|
377
|
+
this.discoveryLogger.warn(`Glob pattern failed: ${pattern}`, String(error));
|
|
378
|
+
return this.findMatchingFilesFallback(searchPath, patterns, ignorePatterns, maxDepth);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// fs.glob not available, fall back to manual file discovery
|
|
383
|
+
this.discoveryLogger.debug('Native fs.glob not available, using fallback');
|
|
384
|
+
return this.findMatchingFilesFallback(searchPath, patterns, ignorePatterns, maxDepth);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return [...new Set(allFiles)]; // Remove duplicates
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Fallback for Node.js versions without fs.glob
|
|
391
|
+
private async findMatchingFilesFallback(
|
|
392
|
+
searchPath: string,
|
|
393
|
+
patterns: string[],
|
|
394
|
+
ignorePatterns: string[],
|
|
395
|
+
maxDepth: number = 5
|
|
396
|
+
): Promise<string[]> {
|
|
397
|
+
const config = {
|
|
398
|
+
patterns,
|
|
399
|
+
ignorePatterns,
|
|
400
|
+
maxDepth,
|
|
401
|
+
recursive: true,
|
|
402
|
+
} as ModuleDefaultsConfig['autoDiscovery'];
|
|
403
|
+
|
|
404
|
+
// Handle both absolute and relative paths
|
|
405
|
+
const fullSearchPath = isAbsolute(searchPath) ? searchPath : join(this.baseDir, searchPath);
|
|
406
|
+
|
|
407
|
+
// Check if search path exists
|
|
408
|
+
try {
|
|
409
|
+
const { access } = await import('fs/promises');
|
|
410
|
+
await access(fullSearchPath);
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Get files and convert to relative paths
|
|
416
|
+
const files = this.findMatchingFiles(fullSearchPath, config);
|
|
417
|
+
const relativeFiles = files.map(file => relative(this.baseDir, file));
|
|
418
|
+
|
|
419
|
+
return relativeFiles;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Simple pattern matching for fallback (basic glob support)
|
|
423
|
+
private matchesSimplePattern(path: string, pattern: string): boolean {
|
|
424
|
+
try {
|
|
425
|
+
// Normalize path separators
|
|
426
|
+
const normalizedPath = path.replace(/\\/g, '/');
|
|
427
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
428
|
+
|
|
429
|
+
// Convert simple glob patterns to regex
|
|
430
|
+
const regexPattern = normalizedPattern
|
|
431
|
+
.replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace ** BEFORE escaping
|
|
432
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex chars
|
|
433
|
+
.replace(/\\\*/g, '[^/]*') // * matches anything except /
|
|
434
|
+
.replace(/___DOUBLESTAR___/g, '.*') // ** matches anything including /
|
|
435
|
+
.replace(/\\\?/g, '[^/]') // ? matches single character except /
|
|
436
|
+
.replace(/\\\{([^}]+)\\\}/g, '($1)') // {ts,js} -> (ts|js)
|
|
437
|
+
.replace(/,/g, '|'); // Convert comma to OR
|
|
438
|
+
|
|
439
|
+
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
|
440
|
+
const result = regex.test(normalizedPath);
|
|
441
|
+
|
|
442
|
+
return result;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
this.discoveryLogger.warn(`Pattern matching error for "${pattern}": ${String(error)}`);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
305
449
|
// Check if path should be ignored
|
|
306
450
|
private shouldIgnore(path: string, ignorePatterns: string[]): boolean {
|
|
307
|
-
return ignorePatterns.some(pattern =>
|
|
451
|
+
return ignorePatterns.some(pattern => this.matchesSimplePattern(path, pattern));
|
|
308
452
|
}
|
|
309
453
|
|
|
310
454
|
// Check if path matches any of the patterns
|
|
311
455
|
private matchesPatterns(path: string, patterns: string[]): boolean {
|
|
312
|
-
return patterns.some(pattern =>
|
|
456
|
+
return patterns.some(pattern => this.matchesSimplePattern(path, pattern));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check if file is within max depth (for glob results)
|
|
460
|
+
private isWithinMaxDepth(relativePath: string, searchPath: string, maxDepth: number): boolean {
|
|
461
|
+
// Count directory separators to determine depth
|
|
462
|
+
const pathFromSearch = relative(searchPath, join(this.baseDir, relativePath));
|
|
463
|
+
const depth = pathFromSearch.split('/').length - 1; // -1 because file itself doesn't count as depth
|
|
464
|
+
return depth <= maxDepth;
|
|
313
465
|
}
|
|
314
466
|
|
|
315
467
|
// Remove duplicate modules
|