scan function
Scans the project filesystem and emits typed scan events.
Implementation
Stream<ScanEntry> scan(BuildConfig config) async* {
final root = config.rootDir;
final routesRoot = Directory(p.join(root, config.routesDir));
final middlewareRoot = Directory(p.join(root, config.middlewareDir));
final hooksFile = File(p.join(root, 'hooks.dart'));
final semantics = ResolvedScannerContext(root);
try {
if (await middlewareRoot.exists()) {
await for (final file in _discoverDartFiles(
middlewareRoot,
recursive: false,
)) {
final parsed = _parseScopedHandlerFile(
p.basename(file.path),
expectedBaseName: null,
)!;
final entry = MiddlewareEntry(
filePath: file.path,
path: '/**',
method: parsed.method,
);
await validateMiddlewareHandler(semantics, entry.filePath);
yield ScanEntry.globalMiddleware(entry);
}
}
final seenRoutes = <String, String>{};
final seenShapes = <String, _ShapeRecord>{};
final catchAllKindsByDir = <String, bool?>{};
if (await routesRoot.exists()) {
await for (final file in _discoverDartFiles(
routesRoot,
recursive: true,
)) {
final relativePath = p.relative(file.path, from: routesRoot.path);
final segments = p.split(relativePath);
final fileName = segments.last;
final dirSegments = segments.take(segments.length - 1).toList();
if (dirSegments.any((segment) => segment.startsWith('_'))) {
continue;
}
final scopedMiddlewareFile = _parseScopedHandlerFile(
fileName,
expectedBaseName: '_middleware',
);
if (scopedMiddlewareFile != null) {
final entry = MiddlewareEntry(
filePath: file.path,
path: _scopePath(dirSegments),
method: scopedMiddlewareFile.method,
);
await validateMiddlewareHandler(semantics, entry.filePath);
yield ScanEntry.scopedMiddleware(entry);
continue;
}
final scopedErrorFile = _parseScopedHandlerFile(
fileName,
expectedBaseName: '_error',
);
if (scopedErrorFile != null) {
final entry = ErrorEntry(
filePath: file.path,
path: _scopePath(dirSegments),
method: scopedErrorFile.method,
);
await validateErrorHandler(semantics, entry.filePath);
yield ScanEntry.scopedError(entry);
continue;
}
if (fileName.startsWith('_')) {
continue;
}
final parsed = _parseRouteFile(relativePath);
final dirKey = p.dirname(relativePath);
if (parsed.catchAllKind != null) {
final previous = catchAllKindsByDir[dirKey];
if (previous != null && previous != parsed.catchAllKind) {
throw RouteScanException(
'Conflicting catch-all files in "$dirKey": both named and unnamed catch-all routes are present.',
);
}
catchAllKindsByDir[dirKey] = parsed.catchAllKind;
}
final routeKey = '${parsed.method ?? '*'} ${parsed.path}';
final previous = seenRoutes[routeKey];
if (previous != null) {
throw RouteScanException(
'Duplicate route "$routeKey" declared by "$previous" and "$relativePath".',
);
}
seenRoutes[routeKey] = relativePath;
final shapeKey = parsed.shapePath;
final shapeRecord = seenShapes[shapeKey];
if (shapeRecord != null &&
!_sameNames(shapeRecord.names, parsed.paramNames)) {
throw RouteScanException(
'Param-name drift for route shape "$shapeKey": "${shapeRecord.source}" and "$relativePath".',
);
}
seenShapes.putIfAbsent(
shapeKey,
() => _ShapeRecord(relativePath, parsed.paramNames),
);
await validateRouteHandler(semantics, file.path);
final openapi = await scanRouteOpenApiMetadata(semantics, file.path);
final entry = RouteEntry(
filePath: file.path,
path: parsed.path,
method: parsed.method,
wildcardParam: parsed.wildcardParam,
openapi: openapi,
);
if (parsed.isRootFallback) {
yield ScanEntry.fallback(entry);
continue;
}
yield ScanEntry.route(entry);
}
}
if (await hooksFile.exists()) {
yield ScanEntry.hooks(await scanHooksMetadata(semantics, hooksFile.path));
}
} finally {
await semantics.dispose();
}
}