Hydration Proof

Search documentation

Find a page or section

Every app in the repository, one command, one report.

To run hydration tests in a monorepo, put a hydration-proof config at the repository root that lists your apps under projects. Each app keeps its own config, with its own server, routes, scenarios and CI policy. npx hydration-proof test then tests every app, one after another, and writes one combined report.

How to run hydration tests in a monorepo

A repository with three apps looks like this:

repo/
  hydration-proof.config.ts             lists the projects
  apps/web/hydration-proof.config.ts
  apps/admin/hydration-proof.config.ts
  apps/docs/config/hydration-proof.ts
  1. Give every app a config of its own. Running npx hydration-proof init in an app's folder writes one from that app's package.json and route folders.

  2. List the apps in a config at the repository root:

    hydration-proof.config.ts
    import { defineConfig } from "hydration-proof";
     
    export default defineConfig({
      projects: [
        // A path is enough when the folder has a hydration-proof config.
        "apps/web",
        // A name makes --project admin and the report grouping read better.
        { path: "apps/admin", name: "admin" },
        // Or point at a config that is not in the default place.
        { path: "apps/docs", name: "docs", config: "config/hydration-proof.ts" },
      ],
    });
  3. Run the tests from the root:

    npx hydration-proof test

Why not one config with every route? Because the apps have different servers. hydration-proof starts an app (building it first when there is no build yet), tests it, stops it, and only then moves on to the next one.

Each project

Each entry in projects is a folder path, or an object:

OptionTypeDefaultDescription
path*stringFolder of the project (with its own hydration-proof config).
namestringthe folder nameName shown in reports.
configstringfound automaticallyConfig file inside the folder.

Project names must be unique. When two folders have the same name (say apps/web and packages/web), set name on one of them.

Configure each app on its own

A project config is an ordinary config. Everything about how an app is tested comes from it: the server, routes, scenarios, ignore rules, the baseline and the CI policy.

apps/admin/hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  server: {
    command: "pnpm start --port {port}",
    build: "pnpm build",
    port: 3002,
  },
  routes: { discover: true, dynamic: { "/users/[id]": ["1"] } },
  scenarios: [
    {
      name: "admin",
      cookies: [{ name: "session", value: process.env.ADMIN_SESSION ?? "" }],
    },
  ],
  // The legacy app may keep a shrinking number of warnings.
  ci: { budget: { error: 0, warning: 8 } },
});

A per-project ci.budget lets the app that is already clean stay at zero while a legacy app works its numbers down. See baselines and budgets for the other ways to adopt the tool on an app that already has findings.

The root config decides where the combined report goes and in which formats (outputDir, reporters). Command-line flags such as --mode both or --fail-on warning apply to every project.

Does each app need its own port?

Only if you set one. Without server.port, every app gets a free port and there is nothing to do.

If a config sets server.port, give every app a different one. Outside CI, hydration-proof reuses an app that already answers on a configured port (server.reuseExisting), so two apps on the same port can end up testing the same server.

Test one app with --project

npx hydration-proof test --project admin
npx hydration-proof test --project admin --project docs

--project takes a project's name or its path, and can be repeated. An unknown name stops the run with exit code 2 and lists the projects that exist.

Read the combined report

The root writes one report for all apps, in the formats its reporters list. Findings are grouped by project, so a finding is never ambiguous about which app it came from. Screenshots are copied next to the combined report, and each app also keeps its own report in its own output folder.

The run fails when any project fails. If an app cannot be built or started, the other apps are still tested, and the run exits with that app's code (3), which takes precedence over findings (1). Exit codes lists them all.

To combine reports yourself, for example from one CI job per app, use hydration-proof merge-reports or mergeReports() from the Node API.

Owners from CODEOWNERS

Findings show who owns them. hydration-proof looks for CODEOWNERS at the repository root (the nearest folder with .git): .github/CODEOWNERS, CODEOWNERS, docs/CODEOWNERS or .gitlab/CODEOWNERS. Every app finds the same file, so findings are attributed to the right team without any configuration. Route owners (owners.routes) go in each app's config.

Test only what a pull request changed

npx hydration-proof test --changed

--changed follows the import graph from the files a pull request touches to the routes that render them, across package boundaries. In a monorepo that is the difference between testing three apps and testing four pages. By default it compares with the pull request's base branch, or main.

A shared package that every app imports still selects every route that uses it. That is correct: a change there can break all of them.