collect
Scan source paths for definePermissions() calls and permission usages, write the catalog and generated barrel, and fail CI on drift.
permdock collect is the build step that turns colocated permission definitions into a central catalog. It is modelled on next-intl's useExtracted extraction: the source of truth is the code where permissions are declared and used, and the catalog is a compile output that is checked in and kept fresh by tooling. See why this model and Larger apps.
Usage
permdock collect # write permissions.catalog.json (+ barrel, see below)
permdock collect --check # exit 1 if the outputs on disk differ from what would be written
permdock collect --src ./src --src ../ui/src --src './node_modules/@acme/*'
permdock collect --watch # rebuild on change (what the Next.js plugin does in next dev)Options come from permdock.config.ts (collect.srcPath, catalog.out) and can be overridden with flags: --src (repeatable) for collect.srcPath, --out <file> for catalog.out.
--watch collects once, then again 50 ms after the last change to a srcPath folder, the config file, or the configured permissions and policy modules. A glob entry watches the folder above its first wildcard. Changes to the catalog and barrel it wrote are ignored, and changes during a run queue one more run. Every run after the first loads the project modules again, so an edited import is picked up. Failures are printed and the watcher keeps running; --watch with --check is a usage error.
What it scans
collect parses every .ts and .tsx file under each srcPath entry with oxc-parser and records two things:
- Definitions: every
definePermissions({...})andresource(...)call, with its file, export name and the static shape of the tree (resource names,actions,collection, action metadata). Schemas are not evaluated during the scan; the catalog's resource schemas come from executing the merged definition module and calling Standard JSON Schema. - Usages: every member access rooted at a known definition export, such as
permissions.post.updateorpostPermissions.post.read, together with the call it feeds (can,decide,assert,usePermission,protect,allow,registerTool, ...). Usages are whatpermdock usageandpermdock doctorreport on.
srcPath entries may point at first-party code, sibling packages in a monorepo and installed packages (./node_modules/@acme/*), exactly as next-intl allows for shared UI packages. Globs are resolved relative to --cwd and match segment by segment (packages/*/src walks only each package's src). Under a glob, node_modules, dist, .next, .turbo and coverage are skipped unless the pattern names them literally; inside an installed package (./node_modules/@acme/*) dist is collected and nested node_modules never is. A file reached twice, for example through a workspace symlink, is collected once.
Usage scanning also records allow(permissions.x.y) calls in policy files, as usages with call: 'allow'. Installed packages are scanned only when a srcPath entry names them.
Scanning is static: dynamic keys, computed member access and permissions resolved through findPermission(permissions, someString) are recorded as dynamic usages and reported separately rather than guessed at.
Outputs
permissions.catalog.json: the catalog described on the catalog page, including for each permission itskey,scope, arity, metadata, defining file and usage sites. Deterministic ordering and formatting so diffs are meaningful.- Generated barrel (
src/permissions.generated.tsby default, only whencollect.barrelis enabled): a// @generatedfile that importspermissionsfrom the configured permissions module (permissionsinpermdock.config.ts, or the first of the conventional paths that exists), relative to the barrel's own folder, and passes it tomergePermissions().
Generated files carry a header with the CLI version and the list of inputs, and are formatted with Oxfmt so they do not churn under the project's formatter.
--check in CI
--check computes the outputs in memory and compares them with the files on disk. Any difference exits 1 and prints a unified diff. This is the permissions equivalent of next-intl checking that target locale files are in sync with the extracted source messages: adding a permission, renaming a resource or changing action metadata without re-running collect fails the build.
- run: pnpm exec permdock collect --check --cwd tests/integration/fixtures/posts
- run: pnpm exec permdock collect --check --cwd apps/examples/monorepoThe Next.js plugin runs collect during next dev and next build, so in a Next.js app the check mostly catches commits made without running the dev server.
Relationship to the runtime
collect does not define permissions. Because permission leaves are runtime values, the merged definition module is the catalog at runtime (listPermissions(permissions)), and collect exists for three things the runtime cannot do: know where permissions are used, know about features that are not yet imported into the app, and produce artifacts (JSON, JSON Schema, Markdown) that CI, docs and agents read without executing code.
Next.js integration
// next.config.ts
import { createPermDockPlugin } from "permdock/next/plugin";
const permdockPlugin = createPermDockPlugin({
collect: { srcPath: ["./src", "../ui/src", "./node_modules/@acme/*"] },
});
export default permdockPlugin({});The plugin is a build hook only; it never wires the API or augments module types. See Next.js plugin.
Why this model
next-intl's useExtracted solved the same shape of problem for translations: many colocated typed declarations, one central artifact, client and server consumers. A central string catalog that usages reference by name puts the declaration far from the code, lets keys drift and accumulates dead entries. Extraction flips it, and PermDock takes the same properties:
| next-intl | PermDock |
|---|---|
useExtracted() declaration in a component | definePermissions() in a feature folder |
| Build-time loader | permdock collect, createPermDockPlugin({ collect }) |
messages/*.json as a committed compile output | permissions.catalog.json, checked with --check |
| Namespaces for shared packages | Nested resource groups merged by mergePermissions (permissions.billing.invoice.pay) |
useExtracted / getExtracted | usePermission / getPermission, usePermDock / getPermDock |
| Passing one message namespace to the client | snapshot({ include: [permissions.post] }) |
| Works in tests without the loader | Works in tests without collect |
The analogy stops in three places. next-intl must extract because a string literal is not a queryable runtime value; PermDock's definitions exist at runtime, so the scan is for usage coverage and cross-package discovery, and a failed scan only leaves the catalog stale. Translations sync target locales; permissions have a policy instead, and permdock usage reports the gap (granted-by-no-role, used-but-ungranted) rather than syncing anything. next-intl's loader rewrites source; PermDock only reads it. Generated definitions (permdock rls import, permdock openapi import) merge through mergePermissions like any hand-written feature file.
Related
Last updated on