{"_id":"@avasapp/rozenite-plugin-data-seed","_rev":"2-f9e5bd857f3c8e38066846b5d8937c00","name":"@avasapp/rozenite-plugin-data-seed","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@avasapp/rozenite-plugin-data-seed","version":"0.1.0","keywords":["tanstack-query","react-query","fetch","http","msw","react-native","devtools","rozenite","mocking","fixtures","debugging"],"license":"MIT","_id":"@avasapp/rozenite-plugin-data-seed@0.1.0","maintainers":[{"name":"crockalet","email":"npm.drainage506@passmail.net"},{"name":"athik.dev","email":"athik@avas.dev"}],"homepage":"https://github.com/avas-app/rozenite-plugin-data-seed#readme","bugs":{"url":"https://github.com/avas-app/rozenite-plugin-data-seed/issues"},"bin":{"data-seed":"bin/data-seed.mjs"},"dist":{"shasum":"38d03aabe33e7b00dfa0c4d2c893e86925ad6f93","tarball":"https://registry.npmjs.org/@avasapp/rozenite-plugin-data-seed/-/rozenite-plugin-data-seed-0.1.0.tgz","fileCount":25,"integrity":"sha512-V+s78bHdR+zF3lF+pbf78CGueGlqxlbfLP38zcY8oB94CXV2Qsh1KxczeBc5GYG8WnHM6PdanX1jnIhyhM9VmQ==","signatures":[{"sig":"MEYCIQDId1WenGC8sEuPCpFOp+1iSXe0ZWMul9vXItDJsMwBAQIhANVN8vXkntB8TQJ6yli1dvuT1lumpR47KLgrpVeLeyRo","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":940875},"main":"./dist/react-native/index.cjs","type":"module","types":"./dist/react-native/index.d.ts","module":"./dist/react-native/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/react-native/index.d.ts","import":"./dist/react-native/index.js","require":"./dist/react-native/index.cjs"},"./sdk":{"types":"./dist/sdk/index.d.ts","import":"./dist/sdk/index.js","require":"./dist/sdk/index.cjs","development":"./sdk.ts"},"./expo":{"types":"./expo.ts","default":"./expo.ts"},"./package.json":"./package.json"},"gitHead":"3bb8a73578d03c9ae693ed2fbb3fcf79035429be","scripts":{"dev":"rozenite dev","docs":"bun run scripts/build-token-docs.ts","test":"bun test src/ bin/","build":"rozenite build","presets":"bun run scripts/build-dev-presets.ts","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"tsc -p tsconfig.json --noEmit && rozenite build"},"_npmUser":{"name":"crockalet","email":"npm.drainage506@passmail.net"},"repository":{"url":"git+https://github.com/avas-app/rozenite-plugin-data-seed.git","type":"git"},"_npmVersion":"11.17.0","description":"Seed fake data into a running React Native app from DevTools — TanStack Query cache entries or HTTP responses, generated from your own TypeScript types.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@rozenite/agent-bridge":"2.1.0","@rozenite/agent-shared":"2.1.0","@rozenite/plugin-bridge":"^2.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.3.1","axios":"^1.19.0","react":"19.2.0","postcss":"^8.5.6","rozenite":"^2.1.0","react-dom":"19.2.0","@types/bun":"^1.3.14","typescript":"~5.9.3","tailwindcss":"^4.2.2","@rozenite/ui":"^2.1.0","@types/react":"~19.2.2","lucide-react":"^0.263.1","react-native":"0.83.1","@types/react-dom":"~19.1.7","react-native-web":"^0.21.2","@tailwindcss/postcss":"^4.2.2","@rozenite/vite-plugin":"^2.1.0","@tanstack/react-query":"^5.66.0","ts-json-schema-generator":"^2.9.0"},"peerDependencies":{"react":"*","react-native":"*","ts-json-schema-generator":">=2"},"peerDependenciesMeta":{"ts-json-schema-generator":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/rozenite-plugin-data-seed_0.1.0_1787222503327_0.3200690597053346","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@avasapp/rozenite-plugin-data-seed@0.2.0","bin":{"data-seed":"bin/data-seed.mjs"},"bugs":{"url":"https://github.com/avas-app/rozenite-plugin-data-seed/issues"},"dist":{"shasum":"17d0e56bc930ec769932d7310db22bd753151d7a","tarball":"https://registry.npmjs.org/@avasapp/rozenite-plugin-data-seed/-/rozenite-plugin-data-seed-0.2.0.tgz","fileCount":99,"integrity":"sha512-K/yRgVHqaL2721NcE0Bo49TgXR2PdM2uSfj21hoOgJlF/n58gLZNc+7xymObSW9UZbh7XR7lk3PdaGc+0yS0jA==","signatures":[{"sig":"MEQCIGX5vEbm2eF5CK2uJTxgYVQ9utxyg9WQFWRVbmYfh6w7AiBgEkQTEj2lmZir2X7xYIkrzQDKxia85Dd7C/wYQIS/vA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCID4Hgh3FbVyiAyA1DmKp823SJTlubpxIcdUVklEm6XbEAiAjVHbAJlHfaY3lszG1FPZEJ1bYJ60rX6u8ZE9tp89h5A=="}],"unpackedSize":1286630},"main":"./dist/react-native/cjs/react-native.js","name":"@avasapp/rozenite-plugin-data-seed","type":"module","types":"./dist/react-native/react-native.d.ts","module":"./dist/react-native/react-native.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/react-native/react-native.d.ts","import":"./dist/react-native/react-native.js","require":"./dist/react-native/cjs/react-native.js"},"./sdk":{"types":"./dist/sdk/sdk.d.ts","default":"./dist/sdk/sdk.js","development":"./sdk.ts"},"./expo":{"types":"./expo.ts","default":"./expo.ts"},"./package.json":"./package.json"},"license":"MIT","scripts":{"dev":"rozenite dev","docs":"bun run scripts/build-token-docs.ts","test":"bun test src/ bin/","build":"rozenite build","presets":"bun run scripts/build-dev-presets.ts","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"tsc -p tsconfig.json --noEmit && rozenite build"},"version":"0.2.0","_npmUser":{"name":"crockalet","email":"npm.drainage506@passmail.net"},"homepage":"https://github.com/avas-app/rozenite-plugin-data-seed#readme","keywords":["tanstack-query","react-query","fetch","http","msw","react-native","devtools","rozenite","mocking","fixtures","debugging"],"repository":{"url":"git+https://github.com/avas-app/rozenite-plugin-data-seed.git","type":"git"},"_npmVersion":"11.17.0","description":"Seed fake data into a running React Native app from DevTools — TanStack Query cache entries or HTTP responses, generated from your own TypeScript types.","directories":{},"maintainers":[{"name":"crockalet","email":"npm.drainage506@passmail.net"},{"name":"athik.dev","email":"athik@avas.dev"}],"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.3.1","axios":"^1.19.0","react":"19.2.0","postcss":"^8.5.6","rozenite":"^2.4.0","react-dom":"19.2.0","@types/bun":"^1.3.14","typescript":"~5.9.3","tailwindcss":"^4.2.2","@rozenite/ui":"^2.4.0","@types/react":"~19.2.2","lucide-react":"^0.263.1","react-native":"0.83.1","@types/react-dom":"~19.1.7","react-native-web":"^0.21.2","@tailwindcss/postcss":"^4.2.2","@rozenite/vite-plugin":"^2.4.0","@tanstack/react-query":"^5.66.0","@rozenite/agent-bridge":"^2.4.0","@rozenite/agent-shared":"^2.4.0","@rozenite/plugin-bridge":"^2.4.0","ts-json-schema-generator":"^2.9.0"},"peerDependencies":{"react":"*","react-native":"*","@rozenite/agent-bridge":"^2.2.0","@rozenite/agent-shared":"^2.2.0","@rozenite/plugin-bridge":"^2.2.0","ts-json-schema-generator":">=2"},"peerDependenciesMeta":{"ts-json-schema-generator":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rozenite-plugin-data-seed_0.2.0_1790161942344_0.17240670526432722"}}},"time":{"created":"2026-08-20T10:41:43.177Z","modified":"2026-09-23T11:12:22.646Z","0.1.0":"2026-08-20T10:41:43.494Z","0.2.0":"2026-09-23T11:12:22.450Z"},"bugs":{"url":"https://github.com/avas-app/rozenite-plugin-data-seed/issues"},"license":"MIT","homepage":"https://github.com/avas-app/rozenite-plugin-data-seed#readme","keywords":["tanstack-query","react-query","fetch","http","msw","react-native","devtools","rozenite","mocking","fixtures","debugging"],"repository":{"url":"git+https://github.com/avas-app/rozenite-plugin-data-seed.git","type":"git"},"description":"Seed fake data into a running React Native app from DevTools — TanStack Query cache entries or HTTP responses, generated from your own TypeScript types.","maintainers":[{"name":"crockalet","email":"npm.drainage506@passmail.net"},{"name":"athik.dev","email":"athik@avas.dev"}],"readme":"# @avasapp/rozenite-plugin-data-seed\n\nAn interactive data seeder for React Native DevTools, built on\n[Rozenite](https://www.rozenite.dev).\n\nStop committing sample data to see a screen. Open DevTools, pick a target, paste\nJSON, and it lands in your app — and **stays** there until you take it out.\n\nTwo things can be seeded:\n\n- **TanStack Query cache entries**, which survive refetches, invalidation, and\n  app focus.\n- **HTTP responses**, by intercepting `fetch`, `XMLHttpRequest` (so, axios) and\n  `expo/fetch`. Works with no query cache at all, and lets you force a 500.\n\nPlus:\n\n- **Committed fixtures** that ride in the app bundle, so teammates get them with\n  no setup.\n- **Generate from your TypeScript types**, annotated in-source with `@fake` JSDoc tags.\n- **Drive it headlessly** from a script or an E2E run, with no DevTools open.\n\n## Install\n\n```bash\nnpm install --save-dev @avasapp/rozenite-plugin-data-seed\n```\n\nRequires **Rozenite 2.2 or later**. The Rozenite bridges are peer dependencies,\nso the plugin binds to the copy your app already has — installing it never pulls\na second agent bridge, which would give the app two tool registries and let the\nplugin register into the one the CLI is not talking to.\n\nRozenite discovers the plugin automatically — no `metro.config` change is needed\nbeyond having Rozenite itself set up. TanStack Query v5 is optional; so is having\na query library at all.\n\n## Usage\n\nCall the hook once, anywhere in your component tree:\n\n```ts\nimport { useSeeder } from '@avasapp/rozenite-plugin-data-seed'\n\nfunction DevTools() {\n  useSeeder({ queryClient, http: true })\n  return null\n}\n```\n\nBoth sources are optional and independent. `{ queryClient }` alone seeds the\ncache; `{ http: true }` alone seeds responses and needs no query library.\n\nIt is a no-op outside `__DEV__`, and a no-op while everything is absent, so it is\nsafe to call before the client exists:\n\n```ts\nuseSeeder({ queryClient: isReady ? queryClient : null })\n```\n\nThen open React Native DevTools (`j` from the Metro terminal) and pick the\n**Data Seed** tab.\n\nThat is enough to seed by hand. Two more options unlock the rest —\n[`fixtures`](#fixtures) for committed states and [`schemas`](#generating-from-your-types)\nfor generation:\n\n```ts\nuseSeeder({\n  queryClient,\n  http: true,\n  fixtures: require.context('./seeds', false, /\\.json$/),\n  schemas: require('./data-seed.schemas.json'),\n})\n```\n\n## Which layer to seed\n\nBoth adapters can cover the same screen, and they are not equivalent.\n\n**Seed the cache** when you want to bypass everything below it, or when the data\nnever came from HTTP in the first place. Identity is your own query key, so it\nreads like your source and does not care what the URL is.\n\n**Seed the response** when you want the app's real code to run. A seeded response\nstill goes through your parsing, your `select`, your transform, and your error\nhandling on the way up — all of which a seeded cache entry skips. It is also the\nonly option for `fetch` calls that no query library ever sees.\n\nIf you are testing \"does this screen render 200 items\", seed the cache. If you\nare testing \"does this screen survive what the server actually sends\", seed the\nresponse.\n\n### Why cache seeds stick\n\nThe obvious implementation is `queryClient.setQueryData(key, fake)`. It works for\nabout four seconds — the next window focus, remount, or `invalidateQueries`\nrefetches the key and silently replaces your data with whatever the server says.\n\nThe next idea is `setQueryDefaults(key, { queryFn })`. That does not work at all:\nTanStack resolves options as `{ ...queryDefaults, ...observerOptions }`, so the\n`queryFn` every `useQuery` call passes inline shadows the defaulted one, and the\nseed never fires.\n\nThis plugin wraps `queryClient.defaultQueryOptions` instead, which runs *after*\nthat merge and is therefore the first point where an override actually wins. A\nseeded key resolves to a `queryFn` that returns your data, with `staleTime` and\n`gcTime` pinned to `Infinity` and background refetching switched off.\n\nTwo consequences worth knowing:\n\n- **Seeds are exact-key, not prefix.** Seeding `[\"todos\"]` does not affect\n  `[\"todos\", \"detail\", 1]`. `setQueryDefaults` would have matched both.\n- **Withdrawing a seed refetches.** Removing it leaves stale fake data in the\n  cache, so the plugin invalidates the key to force real data back in.\n\n## Seeding HTTP\n\n`http: true` patches `globalThis.fetch` **and** `XMLHttpRequest`. A seeded route\nis answered locally and never reaches the network; everything else passes\nthrough untouched and is recorded so you can see what your app actually calls.\n\n```ts\nuseSeeder({ http: true })\n```\n\nThat covers `fetch`, everything built on it (ky, ofetch, graphql-request), and\neverything built on XHR — **axios** being the one that matters. React Native's\n`fetch` is itself a polyfill over XHR, so a single request would otherwise be\nrecorded twice; it is counted once, at the fetch layer.\n\n`expo/fetch` needs one extra import — see [below](#expofetch-needs-one-import).\n\nRoutes are matched by pattern, so one seed covers a family of URLs:\n\n| Pattern | Matches |\n| --- | --- |\n| `GET /api/todos` | that path on any host, with or without a query string |\n| `/api/todos` | that path, any method |\n| `GET /api/users/*` | `/api/users/7` — but **not** `/api/users/7/posts` |\n| `GET /api/**` | everything under `/api`, across segments |\n| `GET https://api.example.com/**` | only that host |\n\n`*` stays inside one path segment and `**` crosses them, for the same reason key\npatterns must match length exactly: without the distinction, a list schema\nquietly starts generating data for a detail route.\n\n**Set the status** next to the Apply button to answer with a 500, a 404, or a\n429. Forcing an error is most of the reason to seed a request rather than a cache\nentry, and it is the one state a real backend will not give you on demand.\n\n### The route list is observed, not enumerated\n\nA query cache can be listed before anything happens. HTTP cannot — a route only\nbecomes known once a request has gone out. So the panel shows what it has *seen*,\nwith a hit count, plus any seeded pattern nothing has matched yet.\n\nTo seed something that has never been requested — an endpoint behind an error\npath you cannot reach — type it into the box at the top of the HTTP section. That\nis the case the observed list cannot cover on its own.\n\n### `expo/fetch` needs one import\n\n`expo/fetch` is **native** — it goes through neither `globalThis.fetch` nor\n`XMLHttpRequest`, so neither patch reaches it. One line, once, anywhere in your\nentry file:\n\n```ts\nimport '@avasapp/rozenite-plugin-data-seed/expo'\n```\n\nThat is the whole integration. Call sites keep calling `expo/fetch` normally,\nand order does not matter — the patch is read per request, not captured at\nimport.\n\n<details>\n<summary>Why it is a separate import rather than automatic</summary>\n\nExpo's module export cannot be replaced: Metro compiles `export * from './fetch'`\nto a getter with `configurable: false`, so both assignment and\n`Object.defineProperty` throw. That getter does forward to the *inner* module on\nevery read, and the inner module's export is an ordinary writable property — so\npatching there is visible through the public path.\n\nReaching it means a literal `require` of a path inside Expo, which Metro resolves\nat **bundle** time. Doing that from the main entry would make Expo a build-time\ndependency of this package and break every bare React Native app that installed\nit. A separate entry point is pulled into a bundle only by apps that ask for it.\n\nIf a future Expo release moves that file, the build fails with an unresolved\nmodule naming it — loud, not silent. Fall back to wrapping it yourself, which\ntouches no Expo internals:\n\n```ts\nimport { fetch as expoFetch } from 'expo/fetch'\nimport { seedableFetch } from '@avasapp/rozenite-plugin-data-seed'\n\nexport const fetch = seedableFetch(expoFetch)\n```\n\n`seedableFetch` also works for any other fetch you hold yourself.\n\n</details>\n\n### What it still does not intercept\n\n- **WebSockets**, and anything using a native networking module directly.\n- **A `fetch` you captured before the hook mounted.** `const f = fetch` at module\n  scope keeps the original. Call `fetch(...)` normally, or wrap it with\n  `seedableFetch`.\n\nMetro's own dev endpoints (`/symbolicate`, `/hot`, `/inspector/**`) are excluded\nby default so they do not flood the list. The exclusions are deliberately narrow\npaths rather than \"anything on localhost\", since plenty of people develop against\na local API. Override with `include` / `exclude`:\n\n```ts\nuseSeeder({ http: { include: ['https://api.example.com/**'] } })\n```\n\n`xhr: false` leaves `XMLHttpRequest` alone, if another tool already owns it.\n\n## What the panel shows\n\nThe target list carries only a **preview** of each value — the SDK never sends\nfull values unprompted, so a 40k-row feed costs the same as a settings object.\nOpening a target fetches its real value into the editor on demand.\n\nRows are grouped by adapter once you have more than one. Query rows are annotated\nwith observer count: `inactive` means nothing on screen is subscribed, which is\nworth knowing before you seed it and wonder why nothing changed. HTTP rows show a\nhit count instead.\n\n## Fixtures\n\nA seed you have to retype is a seed you will not reuse. Point the hook at a\nfolder and every JSON file in it becomes a named fixture you can restore in one\nclick:\n\n```ts\nuseSeeder({\n  queryClient,\n  fixtures: require.context('./seeds', false, /\\.json$/),\n})\n```\n\n`./seeds` is the documented default — change it by changing that path. It has to\nbe a literal, because Metro resolves `require.context` statically.\n\n**That one line is the whole setup, and only one person has to write it.**\nFixtures ride in the app bundle, so a teammate who clones the repo opens the\npanel and sees the same list with nothing to configure. Add a fixture file and\nMetro re-bundles it in.\n\nReact Native ships no type for `require.context`; `example/require-context.d.ts`\nis three lines you can copy.\n\n### The file format\n\nPlain, diff-friendly JSON — commit them and the whole team gets them:\n\n```json\n{\n  \"version\": 2,\n  \"name\": \"cart with 50 items\",\n  \"target\": [\"cart\", { \"userId\": 7 }],\n  \"savedAt\": \"2026-08-19T10:00:00.000Z\",\n  \"data\": { \"items\": [] }\n}\n```\n\nA route fixture names its route instead, and may carry a status:\n\n```json\n{\n  \"version\": 2,\n  \"name\": \"profile — 503 outage\",\n  \"target\": { \"kind\": \"route\", \"method\": \"GET\", \"url\": \"/v1/profile\" },\n  \"savedAt\": \"2026-08-20T10:00:00.000Z\",\n  \"meta\": { \"status\": 503 },\n  \"data\": { \"error\": \"upstream unavailable\" }\n}\n```\n\nA fixture carries its own target, so restoring one seeds the right thing even if\nthat screen has never been opened. Hand-written fixtures work too — `target` and\n`data` are the only required fields, and a malformed file is reported in the\npanel by name rather than silently skipped.\n\nFiles written by v1 used `queryKey` instead of `target`. Those still read\ncorrectly, so an existing `seeds/` directory needs no migration.\n\n### Saving new fixtures\n\nReading needs nothing. *Writing* is the one thing a bundle cannot do, so the\nfirst time you save, the panel asks for access to the folder — once, through\nChrome's directory picker, remembered afterwards. Two consequences:\n\n- **Saving is Chrome-only.** Fine in practice, since React Native DevTools *is*\n  Chrome. Reading works regardless.\n- **Access needs re-granting after a browser restart.** Chrome downgrades the\n  saved permission to `prompt`, so the panel shows a *Reconnect* button rather\n  than failing silently.\n\nThe picker cannot be pre-navigated to your repo — `showDirectoryPicker` takes a\nwell-known folder or a handle, never a path, because letting a page steer the\ndialog would leak your filesystem layout. Two things soften that: the picker has\na stable id, so Chrome reopens wherever it was last used, and the panel shows\nyour project's absolute path next to the button, which pastes into the dialog\nwith ⇧⌘G. The path is resolved through Metro's `/symbolicate`; if that fails, no\nhint is shown rather than a wrong one.\n\nYou can also just write the file yourself. Nothing about a fixture requires the\npanel to have created it.\n\n## Generating from your types\n\nTyping JSON by hand is still typing sample data. Point the extractor at your\nTypeScript types once and the panel can generate a whole response instead.\n\n```bash\nnpm install --save-dev ts-json-schema-generator   # optional peer, only for this\nnpx data-seed extract\n```\n\nIt reads `data-seed.config.json`:\n\n```json\n{\n  \"tsconfig\": \"./tsconfig.json\",\n  \"source\": \"./api.ts\",\n  \"out\": \"./data-seed.schemas.json\",\n  \"targets\": [\n    { \"key\": [\"todos\"],          \"type\": \"ApiResponse<Todo[]>\" },\n    { \"key\": [\"user\", \"*\"],      \"type\": \"ApiResponse<User>\" },\n    { \"route\": \"GET /v1/profile\", \"type\": \"Profile\" },\n    { \"name\": \"RealtimePayload\",  \"type\": \"RealtimePayload<PresenceEvent>\" }\n  ]\n}\n```\n\nTwo things decide whether a target works:\n\n- **`source` has to reach every target type.** One module is imported for all of\n  them, so a type that is not exported from it resolves to `any`. In a monorepo\n  that usually means a package specifier (`\"source\": \"@app/state/queries\"`) or a\n  small file that re-exports from wherever the types actually live.\n- **A route pattern is the path on the wire**, including whatever base path your\n  client prepends. If requests go to `https://api.example.com/v2/api/wallet`,\n  then `GET /wallet` never matches and `GET /v2/api/wallet` does. `**` crosses\n  segments, so `GET **/wallet` works when the base varies by environment.\n\nA target that cannot be resolved is named and skipped — the rest are still\nwritten, and the exit code is non-zero so CI notices:\n\n```\n  ✗ [\"emergency-contacts\"]  EmergencyContact[]\n      its elements carry no type information\n      is `EmergencyContact` exported from ./api.ts? A type that is not\n      exported resolves to `any`.\n```\n\nThat check exists because such a schema is otherwise indistinguishable from a\nworking one: it extracts, commits, and then generates `null` for every field.\n\nThen pass the result to the hook, alongside your fixtures:\n\n```ts\nuseSeeder({\n  queryClient,\n  http: true,\n  fixtures: require.context('./seeds', false, /\\.json$/),\n  schemas: require('./data-seed.schemas.json'),\n})\n```\n\nSelect a target and a **Generate** button appears, with controls for array length\nand which union variant to produce.\n\n### Seeing the shape\n\nClick the type name next to **Generate** and the panel renders the type back as\nTypeScript, so you can check what a response looks like without going to find it\nin the source:\n\n```ts\ntype ApiResponse<Todo[]> = {\n  data: {\n    id: number\n    title: string  // @fake lorem.sentence\n    done: boolean\n    createdAt: string  // @fake date.recent\n  }[]\n  meta: {\n    requestId: string\n    durationMs: number\n  }\n}\n```\n\nTwo things it shows that the source does not, at least not at a glance: which\nfields carry a `@fake` annotation, and which are `any` — the second matters\nbecause those are exactly the fields generation has to leave `null`.\n\nTypes used once are inlined to keep it short. Shared, recursive and union types\nkeep their names, so a comment tree ends at `replies: Comment[]` rather than\nexpanding forever, and a discriminated union reads the way it was written:\n\n```ts\ntype Notification =\n  | { kind: 'mention'; … }\n  | { kind: 'system'; … }\n```\n\n### The target map is the part nothing can infer\n\n`\"key\"` / `\"route\"` → `\"type\"` is written by hand, and there is no way around it:\nTypeScript has no idea that `[\"user\", 7]` returns a `User`, or that\n`GET /v1/profile` returns a `Profile`.\n\nFor keys, `\"*\"` matches any single element, so one entry covers every user, and\npatterns must match the key's length — `[\"todos\"]` never captures\n`[\"todos\", \"detail\", 1]`. For routes, the glob rules\n[above](#seeding-http) apply. In both cases an exact pattern beats a wildcard, so\n`[\"user\", 7]` can have its own schema without depending on file order.\n\nGeneric instantiations work directly. `ApiResponse<Todo[]>` is not a named type\nand cannot be requested from a schema generator, so the CLI writes a temporary\nmodule that names it, extracts, and deletes it.\n\n### Types with no key and no route\n\nSome types are not reachable from either: a realtime envelope that arrives over\na websocket or an Ably channel has no query key and no URL. Name one with\n`\"name\"` and its schema is extracted like any other:\n\n```json\n{ \"name\": \"RealtimePayload\", \"type\": \"RealtimePayload<PresenceEvent>\" }\n```\n\n`\"name\"` is the handle the entry is written under; `\"type\"` is the TypeScript\nexpression, so a generic instantiation gets a short stable name rather than\n`RealtimePayload<PresenceEvent>` spelled out at every lookup. Exactly one of\n`\"key\"`, `\"route\"` and `\"name\"` per target.\n\nIt lands in the schemas file as a tagged pattern, alongside the key and route\nentries:\n\n```json\n{ \"pattern\": { \"name\": \"RealtimePayload\" }, \"type\": \"…\", \"schema\": { … } }\n```\n\n**This plugin cannot seed such a target** — it has no address to intercept, so\nit never appears in the panel and never matches a request. The entry is there\nfor whatever *does* own that transport to read. Extraction is the part worth\nsharing: the `@fake` annotations, the `$ref` graph and the enum resolution are\nthe same work regardless of how the bytes arrive.\n\nOlder readers are unaffected in the way that matters: a plugin predating this\nskips that one entry, names it in a warning, and loads every key and route\nentry around it.\n\n### Controlling the values\n\nAn unannotated `string` becomes lorem text, because a bare `string` genuinely\ncould be a name, a URL, or an ISO date. Say which with a JSDoc tag on the field\nitself:\n\n```ts\nexport type User = {\n  /** @fake number.int({min: 1, max: 9999}) */\n  id: number\n  /** @fake person.fullName */\n  name: string\n  /** @fake internet.email */\n  email: string\n  /** @fake date.past */\n  createdAt: string\n}\n```\n\nAnnotations live in the source **deliberately**. A sidecar file keyed by type\npath rots silently the moment someone renames a field; a JSDoc tag cannot\ndesync, gets reviewed in the same diff as the field, and survives refactors.\n\n**The tag is `@fake`, not `@faker`.** There is no `@faker-js/faker` dependency —\nit is several megabytes for perhaps thirty generators, and the token *names*\nbelow are faker-shaped only so they read the way you would guess. Naming the tag\nafter a library that was never involved sent people looking for faker's full API,\nof which this implements a small, fixed subset. `@faker` is still read, so\nexisting annotations keep working and there is nothing to migrate.\n\n### Every token\n\nThe vocabulary is a fixed list of 23 — there is no larger set behind it.\n**[docs/tokens.md](docs/tokens.md)** has every one with its arguments and an\nexample of what it produces, plus what unannotated fields fall back to.\n\nTwo other ways to see the same list, both closer to where you need it:\n\n- **`npx data-seed tokens`** — in your terminal, next to the source you are\n  annotating.\n- **The tag button beside Generate in the panel** — usually the actual moment\n  you want it, since you are looking at a field the shape preview shows as plain\n  `string` and deciding what to make it.\n\nAnything not on that list warns rather than silently substituting something, and\nthe warning names the closest match — misremembering `name.fullName` for\n`person.fullName` is the common case, and it says so instead of sending you to\ngo and look.\n\n### What it tells you it could not do\n\nGeneration is reported honestly rather than papered over:\n\n- **`any` and `unknown` fields** produce `null` and a warning naming the path.\n  There is nothing to generate from, and inventing a shape would be worse.\n- **Numbers are whole by default.** TypeScript has one numeric type, so an id, a\n  count and a price all extract identically; most API numbers are integers, and\n  `\"id\": 839.05` reads as broken data. Use `@faker number.float` for decimals.\n- **Recursive types stop at a depth cap**, so a comment tree terminates.\n- **Unknown `@fake` tokens** warn instead of silently substituting something.\n\nGeneration is seeded, so the same target and roll always produce the same value —\npressing **Generate** again is what rerolls it.\n\n## Driving it without DevTools\n\nEverything the panel does is also a `rozenite agent` tool, so a test or a script\ncan put the app into a known state with no DevTools window open. That is the\npoint of having committed fixtures — reaching the state is the slow part of an\nE2E run, not asserting on it.\n\n```bash\nnpx rozenite agent targets\nnpx rozenite agent session create\nnpx rozenite agent avasapp/data-seed tools -s <session>\n\nnpx rozenite agent avasapp/data-seed call -s <session> \\\n  --tool '@avasapp/rozenite-plugin-data-seed.apply-fixture' \\\n  --args '{\"fixture\": \"cart with 50 items\"}'\n```\n\n| Tool | What it does |\n| --- | --- |\n| `list-targets` | Everything seedable, summarised — values are not returned |\n| `read-target` | One target's value; `found: false` rather than an error when absent |\n| `apply-seed` | Seed a target and keep it seeded |\n| `clear-seed` / `clear-all-seeds` | Withdraw seeds; seeded queries refetch |\n| `list-fixtures` | Bundled fixtures, plus files that failed to parse |\n| `apply-fixture` | Seed from a committed fixture, by id or name |\n| `generate-seed` | Generate from the target's schema; `dryRun` to preview |\n\nEvery tool names its target with **exactly one** of `queryKey` or `route`:\n\n```bash\n--args '{\"queryKey\": [\"user\", 7], \"data\": {\"name\": \"Ada\"}}'\n--args '{\"route\": \"GET /api/users/*\", \"data\": {}, \"status\": 500}'\n```\n\nWrites report `persistent`. It is `false` when the adapter could not be hooked,\nmeaning the seed is a one-shot write the next fetch erases — worth failing a test\nover, and far easier to diagnose here than three assertions later.\n\nFor typed calls from Node, the descriptors are exported:\n\n```ts\nimport { seedTools } from '@avasapp/rozenite-plugin-data-seed/sdk'\n\nawait session.callTool(seedTools.applyFixture, {\n  fixture: 'cart with 50 items',\n})\n```\n\n## Limitations\n\n- **SWR is not supported yet.** The adapter seam exists for it; SWR's `use`\n  middleware is the equivalent hook point.\n- **`expo/fetch` needs one import**, for the reasons in\n  [`expo/fetch` needs one import](#expofetch-needs-one-import). It is the only\n  transport that cannot be reached without a line of app code, and the only one\n  that reads an internal path of another package.\n- **Authoring fixtures is not scriptable.** Applying them is — see\n  [Driving it without DevTools](#driving-it-without-devtools) — but *creating* a\n  file goes through the browser, so CI cannot write new ones. The write path sits\n  behind a `FixtureStore` interface so a CLI-backed implementation can be added\n  without touching the UI.\n- **Responses, not conversations.** This seeds what a request or a query\n  *resolves to*. It does not sequence responses across calls, mock mutations, or\n  simulate latency — that is a mock server's job, and [MSW](https://mswjs.io)\n  already does it well.\n\n## Example app\n\n`example/` is a real Expo app with a real `QueryClient` and a fake API — no\nnetwork, no API key, no account. That is the point: the plugin exists so you do\nnot have to manufacture backend state to see a screen.\n\n```bash\nbun install && bun run build   # repo root\ncd example && bun install && bun run ios\n```\n\nTwo things worth trying:\n\n- Seed `[\"todos\"]`, then press **Break the API**. The screen keeps rendering your\n  data while every request behind it fails.\n- The last three cards have no React Query in them at all, and each uses a\n  different one of React Native's three networking paths — `fetch`, axios over\n  `XMLHttpRequest`, and native `expo/fetch`. All three point at a `.invalid`\n  host, so they start broken by construction. Seed `GET /v1/profile`,\n  `GET /v1/orders` or `GET /v1/invoice` and they render; set the status to 503\n  and they break again.\n\nIt ships a `seeds/` directory covering the states that are tedious to reach\nagainst a real backend — an empty list, 200 items, every variant of a\ndiscriminated union, a 15-level-deep comment tree, a route outage — plus one\ndeliberately malformed file, so the panel's error surface is exercised too. One\nfixture is left in the v1 format on purpose, so the compatibility claim above is\ndemonstrated rather than asserted.\n\nIts types are also deliberately hostile — a generic `ApiResponse<T>` envelope, an\n`any` leak, a self-recursive comment tree, a discriminated union, and ISO dates\ncarried as `string`. Those are the five shapes that break naive\nTypeScript-to-JSON-Schema extraction.\n\n## Development\n\n```bash\nbun install\nbun test        # 193 tests\nbun typecheck\nbun run build\nbun run presets # regenerate rozenite.config.ts dev presets\nbun run docs    # regenerate docs/tokens.md\n```\n\n`bun dev` starts Rozenite's browser dev host on\n[localhost:8888](http://localhost:8888) for quick panel iteration, using the\npresets in `rozenite.config.ts`. Those are **generated** — that file cannot\nimport anything, since Rozenite evaluates it with `new Function` and no `require`\nin scope, so `scripts/build-dev-presets.ts` drives the real adapters and writes\nthe literals. Editing them by hand is how they drifted from the wire types last\ntime.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}