# core/coordinators/route.coordinator.ts

> 90 lines of code and 22 definitions.

Tree: Project Scripts
Language: typescript
Layer: application
Canonical: https://banes-lab.com/anatomy/scripts#file-scripts-core-coordinators-route-coordinator-ts
Source text: https://banes-lab.com/source/scripts/core/coordinators/route.coordinator.ts.txt

Listed in [core/coordinators](https://banes-lab.com/api/source/scripts/core/coordinators.md), after [core/coordinators/closure.coordinator.ts](https://banes-lab.com/source/scripts/core/coordinators/closure.coordinator.ts.md) and before [core/coordinators/snapshot.coordinator.ts](https://banes-lab.com/source/scripts/core/coordinators/snapshot.coordinator.ts.md).

## Definitions

- `captureEach` (lexical_declaration, line 46)
- `captureWhenListening` (lexical_declaration, line 54)
- `stopServer` (lexical_declaration, line 69)
- `captureRoutes` (lexical_declaration, line 81, exported)
- `WINDOWS` (lexical_declaration, line 16)
- `VITE_CONFIG` (lexical_declaration, line 17)
- `SERVER_LOG` (lexical_declaration, line 18)
- `startServer` (lexical_declaration, line 20)
- `output` (lexical_declaration, line 21)
- `collect` (lexical_declaration, line 26)
- `awaitListening` (lexical_declaration, line 34)
- `elapsed` (lexical_declaration, line 35)
- `earlier` (lexical_declaration, line 48)
- `written` (lexical_declaration, line 49)
- `{ at, port }` (lexical_declaration, line 59)
- `listening` (lexical_declaration, line 60)
- `port` (lexical_declaration, line 82, exported)
- `held` (lexical_declaration, line 83, exported)
- `at` (lexical_declaration, line 89, exported)
- `server` (lexical_declaration, line 90, exported)
- `[outcome]` (lexical_declaration, line 91, exported)
- `released` (lexical_declaration, line 92, exported)

## Contained in

- [core/coordinators](https://banes-lab.com/anatomy/scripts/folder-scripts-core-coordinators.md)

## Uses

- [configuration/strings/route.strings.ts](https://banes-lab.com/source/scripts/configuration/strings/route.strings.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/scripts/folder-scripts-configuration-strings.md)
- [core/coordinators](https://banes-lab.com/anatomy/scripts/folder-scripts-core-coordinators.md)

## Source

```typescript
import type { CaptureOptions, StartedServer } from "#types/route.types";
import { LISTEN_POLL_MS, LISTEN_TIMEOUT_MS } from "#configuration/configs/route.config";
import { awaitRelease, holdersOf, terminateTree } from "@banes-lab/build-scripts/core/adapters/server.adapter.ts";
import { listeningAfter, neverListened, portHeld, portStillHeld } from "#configuration/strings/route.strings";
import { absolutePath } from "@ssot/paths";
import { capturePage } from "#core/coordinators/snapshot.coordinator";
import { join } from "node:path";
import { mkdirSync } from "node:fs";
import { portOf } from "@ssot/secrets";
import process from "node:process";
import { snapshotFor } from "#core/converters/route.converter";
import spawn from "cross-spawn";
import { wait } from "@banes-lab/build-scripts/core/timers/base.timer.ts";
import { writeVerbatim } from "@govlab/canonical-write";

const WINDOWS = "win32";
const VITE_CONFIG = "vite.config.ts";
const SERVER_LOG = "dev-server.log";

const startServer = function startServer(): StartedServer {
    const output: string[] = [];
    const server = spawn("npx", ["vite", "--config", absolutePath("app.member", VITE_CONFIG), "--host"], {
        detached: process.platform !== WINDOWS,
        stdio: ["ignore", "pipe", "pipe"],
    });
    const collect = (chunk: unknown): void => {
        output.push(String(chunk));
    };
    server.stdout?.on("data", collect);
    server.stderr?.on("data", collect);
    return { output, process: server };
};

const awaitListening = async function awaitListening(port: number, startedAt: number): Promise<number | null> {
    const elapsed = performance.now() - startedAt;
    if (holdersOf(port).length > 0) {
        return Math.round(elapsed);
    }
    if (elapsed >= LISTEN_TIMEOUT_MS) {
        return null;
    }
    await wait(LISTEN_POLL_MS);
    return awaitListening(port, startedAt);
};

const captureEach = async function captureEach(binary: string, options: CaptureOptions): Promise<boolean> {
    return options.routes.reduce<Promise<boolean>>(async (prior, route) => {
        const earlier = await prior;
        const written = await capturePage(binary, snapshotFor(options, route));
        return earlier && written;
    }, Promise.resolve(true));
};

const captureWhenListening = async function captureWhenListening(
    binary: string,
    options: CaptureOptions,
    started: { readonly at: number; readonly port: number },
): Promise<boolean> {
    const { at, port } = started;
    const listening = await awaitListening(port, at);
    if (listening === null) {
        process.stderr.write(neverListened(port));
        return false;
    }
    process.stdout.write(listeningAfter(listening));
    return captureEach(binary, options);
};

const stopServer = async function stopServer(server: StartedServer, outDir: string, port: number): Promise<boolean> {
    if (server.process.pid !== undefined) {
        terminateTree(server.process.pid);
    }
    const released = await awaitRelease(port);
    writeVerbatim(join(outDir, SERVER_LOG), server.output.join(""));
    if (!released) {
        process.stderr.write(portStillHeld(port));
    }
    return released;
};

export const captureRoutes = async function captureRoutes(binary: string, options: CaptureOptions): Promise<boolean> {
    const port = portOf("SITE_DEV_PORT");
    const held = holdersOf(port);
    if (held.length > 0) {
        process.stderr.write(portHeld(port, held));
        return false;
    }
    mkdirSync(options.outDir, { recursive: true });
    const at = performance.now();
    const server = startServer();
    const [outcome] = await Promise.allSettled([captureWhenListening(binary, options, { at, port })]);
    const released = await stopServer(server, options.outDir, port);
    if (outcome.status === "rejected") {
        throw outcome.reason instanceof Error ? outcome.reason : new Error(String(outcome.reason));
    }
    return outcome.value && released;
};
```
