Working with code generation, debugging, and troubleshooting
pnpm dev and the shell variants (pnpm dev:responder, pnpm dev:console, pnpm dev:wallboard) run codegen once, when they start. It does not watch for changes. After you add or edit a .graphql file, regenerate the types:
.graphql.ts files.
For frontends participating in the AxonMundi prototype, source the schema from
its composed client schema rather than a single subgraph or local API endpoint.
The established Prepared911 GraphQL stack remains the canonical integration for
other frontends. Refresh the prototype schema before changing operations for a
newly available backend field:
pnpm schema:sync uses the self-hosted Cosmo registry by default. It requires
COSMO_API_URL and either your own pnpm schema:login session or approved CI
credentials. For local, unmerged AxonMundi changes, set AXONMUNDI_DIR after
running just compose in that checkout.
See AxonMundi onboarding for access and
credential boundaries, and Registry and schema workflow
for the full refresh and drift-check process.
For each .graphql file, codegen creates a .graphql.ts file:
useFragment; the types are masked.graphql.ts files.graphql (Vite resolves to .graphql.ts automatically)GetChatroomsDocument
pnpm install. On commit, lint-staged runs the GraphQL checks on staged files: the layout check and graphql-eslint on .graphql files, and the Apollo oxlint rules on TypeScript. To run every check yourself:
pnpm lint:graphql is part of pnpm check, so CI runs it on every PR, uncached. It has three steps. The rules, their rationale, and what is deliberately off are in "GraphQL gate" in LINT-POLICY. Every rule is an error, and there is no suppressions file: fix the document.
packages/tooling-config/bin/check-graphql-layout.mjs fails when a .graphql file:
apps/*/src, remotes/*/src, or packages/*/src (codegen would never read it, so it would get no generated types; a remote's exposes/ directory is not a document location);queries.graphql, mutations.graphql, subscriptions.graphql, or fragments.graphql;eslint.graphql.config.mjs checks each document against the vendored packages/data-gql/src/schema.graphql:
operations-recommended, including no-anonymous-operations, no-unused-variables, no-duplicate-fields, and the unique-* rules.require-selections: select id where the type has one. This is what Apollo's cache normalization needs. A type without an id field is not affected.axon/fragment-type-has-cache-identity: a fragment's type condition needs an id field or keyFields in packages/data-gql/src/cache-key-fields.json; on an interface or union, every type it can stand for does. See Types without an identity.no-deprecated: do not select a deprecated field. Check the schema's deprecation reason for the replacement.naming-convention: PascalCase operations, camelCase variables, no Query/Mutation/Subscription/Fragment suffix, and subscriptions start with On. A fragment is named Component_prop (ChatroomListItem_chatroom): the owning component in PascalCase, an underscore, then the prop in camelCase.selection-set-depth: at most 7 levels. Fragments do not reduce it: the rule counts the fragments a document spreads. Seven fields are skipped because their shape is deep by nature: form, sections, questions (a hierarchical form), activeVersion (recursive versioning), and resource, v1, v2 (audit-log union shapes). The skip list is an escape hatch, not a fix: select less (drop levels the screen does not read, start nearer the data with a root field that takes an id, or split the query at the route), and add a field only if its schema shape is recursive or hierarchical by design (not merely because one query is deep), saying why in the PR. Never add a field only to make a lint error pass. The skip is by field name and applies in every document, and a test pins the list.known-directives: accepts Apollo's client directives (@client, @connection, @nonreactive, @unmask). @unmask is an escape hatch: lint reports every use, and the way through is # eslint-disable-next-line no-restricted-syntax -- <why> on the line above.no-unused-fragments (it only sees spreads inside operations, so it flags a fragment that a component reads with useFragment, which is how every fragment is read under data masking, and it cannot see TypeScript), no-one-place-fragments (contradicts composing a child fragment into one parent), and the schema rules (the schema is vendored).
An oxlint config, oxlint.graphql.config.ts, reports: inline gql (tests and stories may build a deliberately non-generated document), explicit type arguments on Apollo hooks (useQuery<Data, Vars>(...); the generated Document infers them), indexed access on a generated result type (GetThingQuery["thing"][0]; give the component its own fragment instead), imports of ./queries.graphql.ts instead of ./queries.graphql, fetch to a literal /graphql URL (with a no-restricted-syntax selector), and comparing an enum with a string (typescript/no-unsafe-enum-comparison, type-aware, so status === "ACTIVE" and case "ACTIVE": fail; compare with the member, ChatroomStatus.Active).
.graphql file under src/ of an app, remote, or package, so there is no import to add. Common reasons are a misspelled name (a fragment is named Component_prop, such as UserCard_user, not UserData), or a fragment in a file that isn't named fragments.graphql or isn't under a document location, which pnpm lint:graphql reports.
Solution:
documents glob pattern is specificrenderWithApollo from @prepared911/util-testing builds the same client the app uses, including its batching link, and points it at MSW. The MSW handlers from @prepared911/graphql-mocks execute every operation against the vendored schema. An operation or field the schema does not have fails the test, so a mock can't drift from the API. The wiring and the reasons for it are in docs/TESTING.md under "GraphQL mocking".
The rules for tests follow the rules for components:
useQuery.mocks = [{ request, result }] arrays or wrap the component in MockedProvider. The real client talks to schema-driven handlers instead.@prepared911/util-testing and @prepared911/graphql-mocks as dev dependencies, then use util-testing's setup file in the package's Vitest config:
packages/util-testing/vitest.config.ts raises hookTimeout and testTimeout for that reason, and your package may need the same.
Most tests need no mock at all: the default handlers answer any operation in the schema with generated data. Pin data only for the values the test asserts on. Pass createPreparedMockHandlers to graphqlMockServer.use, and the override applies to that test and is reset afterwards.
mocks takes a function per type (DispatchCenter: () => ({ name: "Lakeside" }) applies to every dispatch center) or per field under Query, as above. Fields you leave out are generated. The test asserts on the rendered names, which come from a child component that reads its own fragment, so one test covers the root and its children together.
Don't reach for a raw graphql.query("Name", …) from MSW. It skips schema validation, and it silently does not apply when the client batches requests. createPreparedMockHandlers covers both shapes.
Give a field a resolver that throws to test a failed query or mutation. The resolvers option replaces the field's generated result:
useMutation, interact with it as a user would, and assert on what the user sees. Use a throwing resolver under Mutation for the failure path, and assert that the component shows the error:
FragmentType prop can't be rendered with a plain object: it needs a reference that came out of a query. Today's sanctioned way is a small test-only query that selects the parent field and spreads the component's fragment, plus a probe component that runs that query and hands the component the result. renderWithApollo renders the probe, and the same MSW handlers supply the data. This is the shape of the probe in packages/util-testing/src/render.test.tsx.
The test-only query goes in the component's queries.graphql, next to its fragments.graphql. Give it a name nothing else uses:
useQuery, and it passes the component data.allDispatchCenters[0], which is exactly what the real query root passes:
client.writeFragment({ fragment, fragmentName, data }) with from: { __typename, id }. A seedFragment helper (CORE-1305) is coming as a thin wrapper over it.MockedProvider stand-in.…FragmentDoc does not include the fragments it spreads. A query that spreads several fragments works, because operation documents include them. But useFragment, cache.readFragment and cache.writeFragment on a fragment that itself spreads a child's fragment throw No fragment named <Child>, in tests and in production. Until CORE-1303 (#1238) lands, the component under test, and the recipe above, use a leaf fragment: one with no ...OtherFragment spread inside it. A route's query may spread any number of leaf fragments. This note goes away when CORE-1303 lands.
A hook that calls an Apollo hook needs a client, and renderWithApollo renders elements, not hooks. Render a small probe component that calls the hook and prints what the test asserts on, the same way the probe above calls useQuery:
createPreparedMockHandlers, and let the schema generate the rest.userEvent.setup() for interaction and role or text queries for assertions../queries.graphql, and enums and inputs from @prepared911/data-gql/types.MockedProvider or write { request, result } mock arrays.useQuery, useFragment or other Apollo hooks with vi.mock.FragmentType. It passes the type check and hides whether the fragment and the query agree.docs/TESTING.md - Vitest wiring and GraphQL mockingMockedProvider, which we don't use, so apply only its general advicepnpm turbo run codegen manually)fragments.graphql next to that component, name it Component_prop, and spread it only from the component's parent:
pnpm dev starts; run pnpm turbo run codegen after editing a .graphql file.graphql files next to components.graphql files (Vite resolves to .graphql.ts automatically)pnpm turbo run codegen.graphql.ts files next to .graphql files