render method

Future<List<PrerenderedRoute>> render({
  1. required Uri origin,
  2. required Iterable<String> routes,
  3. required Directory output,
  4. int concurrency = defaultConcurrency,
  5. bool crawlLinks = false,
  6. int maxRoutes = defaultMaxRoutes,
  7. int maxResponseBytes = defaultMaxResponseBytes,
  8. Duration timeout = const Duration(seconds: 30),
})

Renders routes from origin into output.

Explicit routes must be same-origin absolute paths without queries. Link crawling is opt-in. maxRoutes and maxResponseBytes bound work regardless of whether routes are explicit or discovered.

Implementation

Future<List<PrerenderedRoute>> render({
  required Uri origin,
  required Iterable<String> routes,
  required Directory output,
  int concurrency = defaultConcurrency,
  bool crawlLinks = false,
  int maxRoutes = defaultMaxRoutes,
  int maxResponseBytes = defaultMaxResponseBytes,
  Duration timeout = const Duration(seconds: 30),
}) async {
  if (concurrency < 1) {
    throw ArgumentError.value(
      concurrency,
      'concurrency',
      'Must be at least 1.',
    );
  }
  if (maxRoutes < 1) {
    throw ArgumentError.value(maxRoutes, 'maxRoutes', 'Must be at least 1.');
  }
  if (maxResponseBytes < 1) {
    throw ArgumentError.value(
      maxResponseBytes,
      'maxResponseBytes',
      'Must be at least 1.',
    );
  }
  final root = output.absolute;
  final ignoreCase = usesCaseInsensitivePaths(root.path);
  final client = _client ?? HttpClient();
  final queue = <Uri>[];
  final seen = <String>{};
  final outputRoutes = <String, String>{};
  void enqueue(Uri uri, {bool explicit = false}) {
    if (explicit &&
        (!uri.hasAbsolutePath ||
            uri.hasScheme ||
            uri.hasAuthority ||
            uri.hasQuery ||
            uri.hasFragment ||
            uri.pathSegments.any(
              (segment) => segment == '.' || segment == '..',
            ))) {
      throw ArgumentError.value(
        uri,
        'routes',
        'Must be an absolute local path without query or fragment.',
      );
    }
    final normalized = _localRoute(origin, uri);
    if (normalized == null) {
      if (explicit) {
        throw ArgumentError.value(
          uri,
          'routes',
          'Must be an absolute local path without query or fragment.',
        );
      }
      return;
    }
    final route = normalized.toString();
    if (seen.contains(route)) return;
    if (seen.length >= maxRoutes) {
      throw StateError(
        'Prerender route limit of $maxRoutes exceeded while adding "$route".',
      );
    }
    seen.add(route);
    final file = _outputFile(root, normalized);
    final relative = p.relative(file.path, from: root.path);
    final normalizedPath = p.normalize(relative);
    final outputKey = ignoreCase
        ? normalizedPath.toLowerCase()
        : normalizedPath;
    final existing = outputRoutes[outputKey];
    if (existing != null) {
      throw StateError(
        'Prerender routes "$existing" and "$route" both write "$relative".',
      );
    }
    outputRoutes[outputKey] = route;
    queue.add(normalized);
  }

  final generated = <PrerenderedRoute>[];
  var index = 0;

  try {
    for (final route in routes) {
      enqueue(Uri.parse(route), explicit: true);
    }
    output.createSync(recursive: true);
    while (index < queue.length) {
      final end = queue.length;
      Future<void> worker() async {
        while (index < end) {
          final route = queue[index++];
          final page = await _renderRoute(
            client: client,
            origin: origin,
            route: route,
            output: root,
            crawlLinks: crawlLinks,
            maxResponseBytes: maxResponseBytes,
            timeout: timeout,
            enqueue: enqueue,
          );
          generated.add(page);
        }
      }

      final remaining = end - index;
      final workerCount = concurrency < remaining ? concurrency : remaining;
      final workers = List<Future<void>>.generate(
        workerCount,
        (_) => worker(),
      );
      if (workers.isEmpty) break;
      await Future.wait<void>(workers);
    }
  } finally {
    if (_client == null) client.close(force: true);
  }
  generated.sort((left, right) => left.route.compareTo(right.route));
  return generated;
}