generate function

Future<List<GeneratedFile>> generate(
  1. RouteTree tree,
  2. BuildConfig config
)

Generates framework entry files from a scanned route tree.

Implementation

Future<List<GeneratedFile>> generate(RouteTree tree, BuildConfig config) async {
  final outputDir = p.join(config.rootDir, config.outputDir);
  // Generated Dart source files live in a dedicated src/ subdirectory so that
  // compiled output (JS, native binaries) can sit alongside them in separate
  // per-target subdirectories without mixing concerns.
  final genDir = p.join(outputDir, 'src');
  final sourceFiles = <String>{
    for (final entry in tree.routes) entry.filePath,
    for (final entry in tree.globalMiddleware) entry.filePath,
    for (final entry in tree.scopedMiddleware) entry.filePath,
    for (final entry in tree.scopedErrors) entry.filePath,
    ?tree.fallback?.filePath,
    ?tree.hooks?.filePath,
  }.toList()..sort();

  final aliases = <String, String>{};
  final imports = <String>[];
  for (var i = 0; i < sourceFiles.length; i++) {
    final filePath = sourceFiles[i];
    final relative = p.relative(filePath, from: genDir).replaceAll('\\', '/');
    final alias = '\$i$i';
    aliases[filePath] = alias;
    imports.add("import '$relative' as $alias;");
  }

  final routeGroups = <String, List<RouteEntry>>{};
  for (final route in tree.routes) {
    routeGroups.putIfAbsent(route.path, () => <RouteEntry>[]).add(route);
  }

  final openapi = config.openapi;
  final hasDocsUi =
      openapi != null && openapi.ui != null && openapi.output.type == 'route';

  final app = StringBuffer()
    ..writeln("// Generated by spry - do not edit.")
    ..writeln(
      "import 'package:spry/spry.dart' show Spry, ErrorRoute, MiddlewareRoute;",
    );

  final usesHttpMethod =
      hasDocsUi ||
      routeGroups.values.any(
        (entries) => entries.any((it) => it.method != null),
      ) ||
      tree.globalMiddleware.any((it) => it.method != null) ||
      tree.scopedMiddleware.any((it) => it.method != null) ||
      tree.scopedErrors.any((it) => it.method != null);
  if (usesHttpMethod) {
    app.writeln("import 'package:spry/spry.dart' show HttpMethod;");
  }

  for (final line in imports) {
    app.writeln(line);
  }
  if (hasDocsUi) {
    app.writeln("import '_openapi_docs.dart' as \$docs;");
  }

  app
    ..writeln()
    ..writeln('final app = Spry(')
    ..writeln('  caseSensitive: ${config.caseSensitive},')
    ..write(
      config.handlerCacheCapacity == null
          ? ''
          : '  handlerCacheCapacity: ${config.handlerCacheCapacity},\n',
    )
    ..writeln('  routes: {');

  final sortedPaths = routeGroups.keys.toList()..sort();
  for (final path in sortedPaths) {
    final entries = routeGroups[path]!..sort(_compareRouteEntries);
    app.writeln("    '${_escape(path)}': {");
    for (final entry in entries) {
      final key = entry.method == null ? 'null' : _methodLiteral(entry.method!);
      final alias = aliases[entry.filePath]!;
      app.writeln('      $key: ${_handlerLiteral(alias)},');
    }
    app.writeln('    },');
  }
  if (hasDocsUi) {
    final docsRoute = _escape(openapi!.ui!.route);
    app.writeln("    '$docsRoute': {");
    app.writeln('      HttpMethod.get: \$docs.handler,');
    app.writeln('    },');
  }

  app
    ..writeln('  },')
    ..writeln('  middleware: [');

  for (final entry in [...tree.globalMiddleware, ...tree.scopedMiddleware]) {
    final alias = aliases[entry.filePath]!;
    final methodPart = entry.method == null
        ? ''
        : ', method: ${_methodLiteral(entry.method!)}';
    app.writeln(
      "    MiddlewareRoute(path: '${_escape(entry.path)}'$methodPart, handler: $alias.middleware),",
    );
  }

  app
    ..writeln('  ],')
    ..writeln('  errors: [');

  for (final entry in tree.scopedErrors) {
    final alias = aliases[entry.filePath]!;
    final methodPart = entry.method == null
        ? ''
        : ', method: ${_methodLiteral(entry.method!)}';
    app.writeln(
      "    ErrorRoute(path: '${_escape(entry.path)}'$methodPart, handler: $alias.onError),",
    );
  }

  app.writeln('  ],');

  if (tree.fallback case final fallback?) {
    final alias = aliases[fallback.filePath]!;
    app
      ..writeln('  fallback: {')
      ..writeln('    null: ${_handlerLiteral(alias)},')
      ..writeln('  },');
  }

  if (config.target != BuildTarget.cloudflare) {
    app.writeln("  publicDir: '${_escape(config.publicDir)}',");
  }

  app.writeln(');');

  final hooksBuffer = StringBuffer()
    ..writeln("// Generated by spry - do not edit.");
  if (tree.hooks case final hooks?) {
    hooksBuffer
      ..writeln(
        "import '${_relativeImport(hooks.filePath, from: genDir)}' as \$source;",
      )
      ..writeln()
      ..writeln(
        hooks.hasOnStart
            ? 'final onStart = \$source.onStart;'
            : 'final onStart = null;',
      )
      ..writeln(
        hooks.hasOnStop
            ? 'final onStop = \$source.onStop;'
            : 'final onStop = null;',
      )
      ..writeln(
        hooks.hasOnError
            ? 'final onError = \$source.onError;'
            : 'final onError = null;',
      );
  } else {
    hooksBuffer
      ..writeln()
      ..writeln('final onStart = null;')
      ..writeln('final onStop = null;')
      ..writeln('final onError = null;');
  }
  final spec = buildTargetSpec(config);
  final main = _generateMain(spec);
  final files = <GeneratedFile>[
    GeneratedFile(path: 'src/app.dart', content: app.toString()),
    GeneratedFile(path: 'src/hooks.dart', content: hooksBuffer.toString()),
    GeneratedFile(path: 'src/main.dart', content: main),
  ];
  final openApiFile = generateOpenApiDocument(tree, config);
  if (openApiFile != null) {
    files.add(openApiFile);
  }
  if (hasDocsUi) {
    files.add(_generateDocsFile(openapi!));
  }
  files.addAll(spec.extraFiles);
  return files;
}