scan function

Stream<ScanEntry> scan(
  1. BuildConfig config
)

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();
  }
}