@integr8-media/flow-checker

中文文档
0.2.2发布于 2026/11/01MITTypeScriptESM + CJS构建来源已验证

Map a Next.js App Router project: pages, components, requests, types and flows, verified against live traffic and exported as documentation.

原始使用文档
文档目录19 节
  1. What it does
  2. Requirements
  3. Next.js before 15.3
  4. Commands
  5. Dashboard
  6. React Query
  7. Middleware
  8. tRPC
  9. Postman collections
  10. GraphQL
  11. Server-sent events
  12. WebSockets
  13. Setup in detail
  14. The server hook goes first in register()
  15. Configuration
  16. Sensitive data
  17. Files
  18. Limits
  19. License

flow-checker

Maps a Next.js App Router project (its pages, components, requests, types and user flows), checks that map against real traffic while you use the app, and exports it as documentation.

npm install -D @integr8-media/flow-checker
npx flow-checker init     # adds two capture hooks and a config file
npx flow-checker          # scans, opens the dashboard and starts your app with capture on

Then use your app as usual. Each request the app sends shows up in the dashboard, linked to the line of code that sent it.

What it does

  • Scans the code. Routes (groups, dynamic, catch-all, parallel and intercepting segments, next-intl [locale]), the component tree with server/client boundaries, and every request: fetch, axios, your own wrapper clients (and any HTTP library or API client package you list in apiClients), React Query, SWR, tRPC (v10 and v11: hooks, the vanilla client and v11 queryOptions(), typed from the router's procedures), GraphQL (Apollo Client 3 and 4, urql and Relay: operations by name, typed from codegen, the Relay compiler or your schema), server-sent events (new EventSource() and streamed fetch, with the events each listens to and their data types), WebSockets (new WebSocket() and socket.io, with the messages each sends and receives and their types), Server Actions and route handlers (including the second hop to your backend). It also finds navigation between pages (Link, router.push, redirect) and infers user flows such as login → verify OTP → dashboard. Middleware (middleware.ts, or proxy.ts on Next.js 16) is read too: which routes its config.matcher selects, its redirects (with the condition around each one), its rewrites (used to tell which page answered a URL) and the requests it sends. Types of request bodies, responses and props are fully resolved by the TypeScript compiler.
  • Captures real traffic while you test, in the browser and on the server, with passwords, tokens and auth headers masked before anything is saved.
  • Verifies the map. Each capture is matched to the request site that sent it, which marks that site ✓ verified. The captured data is compared with the declared types (missing fields, extra fields, wrong types, unexpected null), and flows are confirmed when you walk them.
  • Checks the code against your Postman collection (optional): requests the collection doesn't have, endpoints nothing calls, and types from saved examples.
  • Keeps up with your edits. Files are watched and re-scanned incrementally. Requests whose code changed since they were captured are marked ↻ needs re-check.
  • Exports documentation: Markdown (with Mermaid route maps and sequence diagrams) and a static, read-only copy of the dashboard you can host anywhere.

Requirements

Supported
Next.js13.4 and later, App Router only (the pages/ directory is not scanned)
Node.js22.12 or later (22.18+ to load a flow-checker.config.ts)
OSWindows, macOS, Linux
Package managersnpm, pnpm, yarn, bun (detected from the lockfile)

Next.js before 15.3

Next.js 15.3 added the instrumentation-client file that loads the browser hook. On 13.4–15.2, wrap your config with withFlowChecker() so the same hook files work (on 13.4–14.x it also turns on experimental.instrumentationHook for the server hook):

// next.config.mjs
import { withFlowChecker } from '@integr8-media/flow-checker/next'

export default withFlowChecker({
  // your config
})
// next.config.js (CommonJS)
const { withFlowChecker } = require('@integr8-media/flow-checker/next')

module.exports = withFlowChecker({})

It changes nothing unless the app was started by flow-checker, and returns your config untouched on 15.3 and later. On these versions browser capture needs webpack: run next dev without --turbo. flow-checker init and flow-checker doctor tell you when you need this.

Commands

flow-checker            # same as `dev`
flow-checker dev        # scan + watch + dashboard + start your app with capture on
flow-checker scan       # scan only, write .flow-checker/model.json
flow-checker init       # add the capture hooks, a config file and .gitignore entries
flow-checker export     # Markdown docs and/or a static HTML site
flow-checker doctor     # check Node, Next.js, hooks, tsconfig, ports and .gitignore
flow-checker postman import   # read a Postman collection to check the code against
OptionCommandsMeaning
--root <dir>allProject root (default: current directory)
--app-dir <dir>allApp Router directory (default: src/app or app)
--config <file>dev, scan, export, doctorConfig file (default: flow-checker.config.* in the root)
--port <n>dev, doctorDashboard port (default 4400)
--app-port <n>dev, doctorPort your app runs on (default 3000)
--dev-command <cmd>devCommand that starts your app (default: the dev script, or next dev)
--no-appdevDon't start the app. Start it yourself with the environment dev prints.
--no-opendevDon't open the browser
--no-watchdevDon't re-scan when files change
-y, --yesinitApply the changes without asking
--tree, --types, --requests, --flowsscanAlso print the route/component trees, resolved types, request sites or flows
--md <dir>, --html <dir>exportWrite Markdown docs / the static site to this folder. Neither: both, to the config's folders.
--rawexportKeep captures as saved, without redacting them again
--forceexportWrite into a folder that has other files
--collection <src>postman importv2.1 JSON file, v3 YAML folder or cloud collection id (default: postman.collection)
--environment <src>postman importEnvironment file or cloud id for {{variables}}
--api-key-env <name>postman importEnv variable with the Postman API key, for cloud ids (default POSTMAN_API_KEY)
--verboseallAlso print the resolved config, where the scan runs and other details
-q, --quietallNo progress output: only results, warnings and errors

Every command shows its progress on stderr. In a terminal, a live line shows a spinner, a bar for the whole command, the current step and its time, and each finished step leaves a ✓ line with what it found:

✓ Loaded: 1,806 files (3.8 s)
✓ Routes: 401 pages, 200 route handlers (0.2 s)
⠸ [█████████░░░░░░░░░░░]  47%  Walking components… route 300/601  1.3 s

In CI, when output is piped, or with NO_COLOR / TERM=dumb, only the ✓ lines are printed, as plain text.

Dashboard

flow-checker dev serves the dashboard on port 4400. The same screens are in the static HTML export, read-only.

  • Overview: counts for pages, route handlers, components, requests, types and flows, each a link to its list, and the things to check (unresolved URLs, unknown triggers, type mismatches...), one note per request.
  • Map: the route map (graph or tree). A page shows its component tree, the requests it sends on load and on events (apart from those its layouts send on every page), where it leads and the flows that start on it.
  • Components: every component, searchable by name or file, filtered by kind (server / client / shared), by what it has (sends requests, things to check, not on any page) and sorted by name, requests or pages.
  • Requests: every request site, searchable by URL, component or file, filtered by when it is sent (on load, on events, server-side, unknown trigger), status (verified, mismatch, needs re-check, not captured yet), transport and flags. The same request sent from several places is one row. A request opens on its own page with its body and response types, server hops, query-cache links, captures and the call path from the component.
  • Flows, Types, Live (captures as they arrive, with their match and type check) and Export.

Filters and searches stay in the URL, so a filtered list can be bookmarked or shared. Lists that grow with the app (the queries a cache call hits, a route handler's callers...) show five entries, then "+N more".

React Query

useQuery, useInfiniteQuery, the suspense hooks, useQueries / useSuspenseQueries (one request per entry), useMutation, usePrefetchQuery and queryClient.fetchQuery / prefetchQuery / ensureQueryData (and their infinite forms) are request sites, in v4 and v5. Each query's key is read into segments (['todos', 'detail', :id]), also through key factories, queryOptions() and functions that return options.

invalidateQueries, refetchQueries, resetQueries, removeQueries and setQueryData are linked to the queries their key hits (by prefix, or exact) and to the mutation whose callback calls them, so a flow shows which queries a mutation refreshes. A server-side prefetchQuery handed over with HydrationBoundary (v4: Hydrate) is linked to the client query it fills. While you test, each request a query or mutation sends carries its key, and captures are matched by it.

Middleware

middleware.ts (and proxy.ts on Next.js 16) is read for its config.matcher: each page shows whether middleware runs on it. NextResponse.redirect() becomes an edge on the route map with the condition around it (if !token), NextResponse.rewrite() and next-intl's default locale are used to tell which page answered a captured URL, and requests the middleware sends get a middleware trigger. On Node.js (proxy.ts) those requests are captured like the rest of the server; on the edge runtime, best effort (see limits).

tRPC

Procedure calls are request sites in v10 and v11: createTRPCReact hooks (trpc.todo.byId.useQuery()), the vanilla client (client.todo.add.mutate()), utils calls and v11's useTRPC() with queryOptions() / mutationOptions(). The router is read from the route handler's fetchRequestHandler() (or the client's AppRouter type), so each call gets its procedure's input type (from .input()) and output type without annotations. While you test, a batched request is split into one capture per procedure, each matched and type-checked on its own.

Postman collections

Point flow-checker at a Postman collection (config postman, or flow-checker postman import --collection <src>) and every scan is checked against it:

  • Each request in the code is matched to a collection request by method and URL ({{baseUrl}}/users/:id fits `${API}/users/${id}`). Requests the collection doesn't have are flagged not in collection; collection requests nothing calls are listed as unused endpoints.
  • Saved response examples give a type to requests whose code has none (marked "from Postman example"), and captures are then checked against it. When the code declares a type, it is checked against the examples (contract mismatch).
  • A URL the scan can't resolve is filled in from the collection request its function names and method point to, when exactly one fits.

postman import prints a summary and the list of findings; the dashboard overview and the Markdown export (postman.md) show them too.

GraphQL

Apollo Client (3 and 4), urql and Relay calls are request sites, one per operation. Every operation goes to the same URL, so captures are matched by operation name, or by persisted query id. Batched requests are split into one capture per operation, and type checks compare variables and data.

Types come from type arguments at the call, codegen's typed documents or the Relay compiler first. Without those, set graphql.schema (or let flow-checker find schema.graphql / the schema in your Relay or codegen config), or graphql.introspection to ask a running endpoint (see config). The dashboard overview lists every operation with its types and callers, and fragments with the components that read them.

Server-sent events

new EventSource(url) and a fetch read as an event stream (Accept: text/event-stream, or any fetch of a route handler that answers with text/event-stream) are request sites. The scan lists the events each one listens to (onmessage, addEventListener('tick', ...)) with the type its data is read as (JSON.parse(e.data) as Tick, const t: Tick = JSON.parse(e.data), MessageEvent<Tick>), and reads the events a route handler writes (event: tick / data: ${JSON.stringify(tick)}, also through a helper like send('tick', tick)). Events the code has no type for take the handler's.

While you test, each stream is captured message by message: the dashboard shows the capture as soon as it opens, then every message with its time, event name and data (redacted like bodies), and each message is checked against its event's type. capture.maxMessages (200) and capture.maxMessageBytes (64 kB) limit what is kept per connection.

WebSockets

new WebSocket(url) and socket.io's io(url) are request sites. The scan lists the messages each one sends and receives with their types: ws.send(JSON.stringify(subscribe)) and JSON.parse(e.data) as Quote for native sockets, socket.emit('chat', draft) and socket.on('chat', (message) => ...) for socket.io (typed events such as Socket<ServerEvents, ClientEvents> give both types).

While you test, every message in both directions is captured (redacted like bodies), with the close code; socket.io events are read from its frames by name. The dashboard shows a timeline of what was sent and received, and each message is checked against its type. capture.maxMessages and capture.maxMessageBytes limit what is kept per connection, as for event streams.

Setup in detail

flow-checker init shows what it will change and asks first:

  • instrumentation-client.ts (browser hook) and instrumentation.ts (server hook), next to your app directory (in src/ when you use src/app). If instrumentation.ts already exports register(), init adds the flow-checker blocks as its first statements (see below). The server hook also registers a small edge runtime, so requests sent from middleware on the edge runtime (middleware.ts before Next.js 16) are captured too; hooks written by 0.1 can get it from init or doctor.
  • flow-checker.config.ts.
  • .gitignore entries for captured data.

Both hooks are no-ops unless the app was started by flow-checker (FLOW_CHECKER=1), and the browser runtime is never active in production builds, so it is safe to commit them.

The server hook goes first in register()

The two flow-checker blocks must be the first statements of register(), outside any if, and register must be async:

// instrumentation.ts
export async function register() {
  // flow-checker: captures server-side requests while you test. It does nothing unless FLOW_CHECKER=1.
  if (process.env.FLOW_CHECKER === '1' && process.env.NEXT_RUNTIME === 'nodejs') {
    const { registerServerRuntime } = await import('@integr8-media/flow-checker/runtime/server')
    registerServerRuntime()
  }
  // flow-checker: requests sent from middleware on the edge runtime (fetch only, best effort).
  if (process.env.FLOW_CHECKER === '1' && process.env.NEXT_RUNTIME === 'edge') {
    const { registerEdgeRuntime } = await import('@integr8-media/flow-checker/runtime/edge')
    registerEdgeRuntime(process.env.FLOW_CHECKER_RUNTIME)
  }

  // your own code
  if (process.env.NODE_ENV === 'production') {
    // ...
  }
}

Pasted inside an existing condition (such as a production-only if) or after an early return, they never run under next dev, and server-side requests are silently not captured.

  • init edits an existing register() for you: it puts the blocks first, makes register async when needed and moves misplaced blocks when run again. A register re-exported from another file gets a note with the lines to add instead.
  • doctor fails when a block is inside an if, switch, loop, try, ternary, && or nested callback, comes after other code, or when register isn't async. It names the line and prints the lines to move.
  • dev warns at start when the hook is misplaced, and once more when your app answers route handlers that send requests but no server-side request is captured within 10 s. Live shows the same warning.

flow-checker dev starts your dev command with FLOW_CHECKER=1, PORT and the capture settings in the environment. With --no-app, it prints that environment so you can start the app yourself (for example in a Docker container or a monorepo task runner).

Configuration

Optional. Everything is auto-detected. See docs/config.md for every option.

// flow-checker.config.ts
import { defineConfig } from '@integr8-media/flow-checker/config'

export default defineConfig({
  root: '.', // project root, relative to this file
  ports: { dashboard: 4400, app: 3000 },
  // Files the scan reads and skips (globs from the root). These are the defaults.
  include: ['**'],
  exclude: ['**/*.test.*', '**/*.spec.*', '**/*.stories.*', '**/__tests__/**', '**/__mocks__/**'],
  // Packages or modules whose calls are requests: HTTP libraries and API clients the scan
  // can't look inside. Default: none.
  apiClients: [{ module: 'ky', methods: ['default', 'get', 'post'] }],
  capture: {
    storeRaw: false, // true = save captures unmasked (local debugging only)
    redact: { keys: ['iban'], headers: ['x-session'] }, // added to the defaults
    ignore: ['/api/health'],
  },
  export: { md: 'docs/flow', html: '.flow-checker/site' },
  // A Postman collection to check the code against (v2.1 JSON, v3 YAML folder or { id, apiKeyEnv }).
  postman: { collection: 'postman/collections/My API', environment: 'postman/environments/dev.yaml' },
  // GraphQL types when the code has none: a schema file, or live introspection (env:NAME headers).
  graphql: { schema: 'schema.graphql' },
})

Event stream and WebSocket capture keep at most capture.maxMessages (200) messages per connection, and messages over capture.maxMessageBytes (64 kB) without their data.

Sensitive data

Redaction is on by default and happens in the runtime, before a capture leaves your app:

  • Headers: authorization, cookie, set-cookie, x-api-key, proxy-authorization.
  • Body keys at any depth (case-insensitive): password, pass, token, accessToken, refreshToken, secret, otp, code, cvv, cardNumber, ssn.
  • Values that look like JWTs, bearer tokens or card numbers, and sensitive query parameters.

Masked values keep their shape ("[REDACTED:string]", 0, false), so type checks still work. The tool server redacts again on arrival, and exports redact again unless you pass --raw.

Files

.flow-checker/
├─ model.json          scan result (safe to commit)
├─ overrides.json      your decisions: manual matches, flow names, recordings (safe to commit)
├─ captures/           captured requests (gitignored)
├─ sessions/           browsing sessions used to confirm flows (gitignored)
├─ cache/              ingest token, code hashes, GraphQL introspection (gitignored)
└─ postman/            the last imported Postman collection (gitignored)

Limits

  • App Router only.
  • WebSockets: only in the browser (not sockets opened on the server). A socket created at module level (outside a component or a function it calls) isn't a request site. socket.io: messages sent over HTTP long-polling before the connection moves to a WebSocket aren't captured (use transports: ['websocket'] to capture everything), acks aren't recorded, binary attachments keep no data, and namespaces aren't told apart when matching.
  • Server-sent events: an EventSource only reports named events someone listens to, so events the app has no listener for aren't recorded. Event streams read by libraries such as @microsoft/fetch-event-source are captured (they use fetch), but their listeners aren't read by the scan. Streams on the edge runtime are captured as plain requests.
  • GraphQL: subscriptions over WebSockets are captured as WebSocket messages, but aren't linked to their operations. Operations sent through your own fetch wrapper (not Apollo, urql or Relay) are plain requests. Server resolvers aren't linked to the operations that run them.
  • Edge runtime (middleware before Next.js 16): capture is best effort, fetch only and without the incoming request. Middleware on Node.js (proxy.ts, or runtime: 'nodejs') is captured like the rest of the server.
  • Middleware config.matcher must be written out in the file (as Next.js requires); routes are matched with sample values for dynamic segments, and redirects or rewrites to URLs built at runtime are shown as written.
  • tRPC: procedures called through createCaller or a server-side createTRPCOptionsProxy({ router }) run without an HTTP request and are not request sites. Subscriptions are found in the code, but their streams are captured as plain requests. The router must be reachable from fetchRequestHandler({ router }) or the client's AppRouter type.
  • Next.js before 15.3: browser capture needs webpack (no Turbopack) and withFlowChecker().
  • Large repositories: the first scan of a ~1,800-file app takes about 12 s and a re-scan after an edit about 4 s. Re-scans run in a worker thread, so the dashboard and capture stay responsive meanwhile.

License

MIT

版本与发布记录

@integr8-media/flow-checker 共发布 5 个版本,首次发布于 2026/10/30;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.2latest2026/11/012 天前
0.2.12026/10/312 天前
0.2.02026/10/313 天前
0.1.02026/10/304 天前
0.0.0-stage2026/10/304 天前

关于 @integr8-media/flow-checker 的常见问题

@integr8-media/flow-checker 是什么?

映射一个Next.js应用路由器项目:页面、组件、请求、类型和流,根据实时流量进行验证,并导出为文档。

如何安装 @integr8-media/flow-checker?

在项目目录运行 npm install @integr8-media/flow-checker,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 flow-checker,可以用 npx flow-checker 直接运行。

@integr8-media/flow-checker 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@integr8-media/flow-checker 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@integr8-media/flow-checker 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@integr8-media/flow-checker 可以商用吗?

@integr8-media/flow-checker 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@integr8-media/flow-checker 还在维护吗?

最新版本 0.2.2 发布于 2026/11/01(2 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/10/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。