render method
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;
}