@rsc-kit/mcp 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/guides/go.md CHANGED
@@ -272,18 +272,49 @@ reg.Changed(ctx, "team:"+teamID+":repos")
272
272
  Every open tab showing the section refreshes it, the moment the webhook
273
273
  lands, with nothing polling. The registry answers `__rsc.changed` by holding
274
274
  the renderer's ask until a version moves - woken at once by a `Changed` in
275
- this process, checking the store every `rsckit.TagsPoll` (1s) for one made
275
+ this process, checking the store every `rsckit.ChangedPoll` (1s) for one made
276
276
  by another instance.
277
277
 
278
- Versions live in a `TagStore`. The default is in memory, which is one
278
+ Versions live in a `VersionStore`. The default is in memory, which is one
279
279
  instance; with more than one, a webhook lands on whichever instance the
280
280
  balancer picked, so give them all the same store:
281
281
 
282
282
  ```go
283
- // CREATE TABLE rsc_tags (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
284
- reg.Names(&rsckit.SQLTags{DB: db, Placeholder: rsckit.Dollar}) // Postgres; nil Placeholder is "?"
283
+ // CREATE TABLE rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
284
+ reg.Versions(&rsckit.SQLVersions{DB: db, Placeholder: rsckit.Dollar}) // Postgres; nil Placeholder is "?"
285
285
  ```
286
286
 
287
+ To hear another instance's change at once instead of within the second - and
288
+ stop reading the store while nothing changes - set `Notify` so each bump
289
+ sends a `pg_notify`, and run a listener with `WakeOn`. The adapter depends on
290
+ no driver, so the listener is yours; with pgx:
291
+
292
+ ```go
293
+ reg.Versions(&rsckit.SQLVersions{DB: db, Placeholder: rsckit.Dollar, Notify: "rsc_versions"})
294
+
295
+ reg.WakeOn(ctx, func(ctx context.Context, connected, wake func()) error {
296
+ conn, err := pool.Acquire(ctx)
297
+ if err != nil {
298
+ return err
299
+ }
300
+ defer conn.Release()
301
+ if _, err := conn.Exec(ctx, "LISTEN rsc_versions"); err != nil {
302
+ return err
303
+ }
304
+ connected()
305
+ for {
306
+ if _, err := conn.Conn().WaitForNotification(ctx); err != nil {
307
+ return err
308
+ }
309
+ wake()
310
+ }
311
+ })
312
+ ```
313
+
314
+ A listener that drops runs again with backoff, and the poll covers the gap.
315
+ Through PgBouncer in transaction mode, connect the listener straight to
316
+ Postgres. One instance needs none of this.
317
+
287
318
  Anything with a `Bump` and a `Versions` fits. Set `RSC_SIGNING_SECRET` for the renderer, the same on every instance:
288
319
  it signs the names a page hands the tab, and is not the host-call secret.
289
320
  Generating and rotating both: [Secrets](/hosts/deployment#secrets). [Sections](/guides/live-data)
@@ -59,6 +59,21 @@ import { cookies } from '@rsc-kit/core/request'
59
59
  export const refreshOn = async () => [`inbox:${(await cookies()).get('agent')?.value}`]
60
60
  ```
61
61
 
62
+ A page with url schemas types its `refreshOn` from them, so `params` arrives
63
+ parsed and typed rather than typed by hand:
64
+
65
+ ```tsx title="src/app/teams/[team]/page.tsx"
66
+ import type { PageRefreshOn } from '@rsc-kit/core/section'
67
+
68
+ export const params = z.object({ team: z.string() })
69
+ export const refreshOn: PageRefreshOn<typeof params> = ({ params }) => [`team:${params.team}`]
70
+ ```
71
+
72
+ `export const refreshOn`, `export function refreshOn` and a list -
73
+ `export { refreshOn } from './names'` - all count. `export * from` does not:
74
+ the build reads the page, not what it re-exports wholesale, so name it in a
75
+ list.
76
+
62
77
  That is the whole feature for an app that is only JavaScript. With a Go or
63
78
  Laravel backend the backend says it instead -
64
79
  [`reg.Changed(ctx, …)`](/hosts/go#saying-data-changed) or
@@ -114,6 +129,55 @@ A section that shows anything per visitor - "you", their unread count, what
114
129
  they may do - must never be `shared`: that is the one place a render is
115
130
  handed from one visitor to another.
116
131
 
132
+ **A change to a child is a change to its parent's list.** Renaming an app
133
+ changes the app's own page and the team's list of apps. Say both where the
134
+ write happens, rather than making either region watch the other's names:
135
+
136
+ ```ts
137
+ await renameApp(app.id, name)
138
+ await changed(`app:${app.id}`, `team:${app.teamId}:apps`)
139
+ ```
140
+
141
+ ```tsx
142
+ // The app's page: refreshOn = ({ params }) => [`app:${params.app}`]
143
+ // The team's list: refreshOn = ({ team }) => [`team:${team}:apps`]
144
+ ```
145
+
146
+ Each region names exactly what it shows, and the write - the one place that
147
+ knows what it touched - names everything that did. A region that watched
148
+ every child's name would hand the tab a name per row, and miss a child added
149
+ after it rendered.
150
+
151
+ **Not everything can announce itself.** A host's runtime logs, a third-party
152
+ status page, anything nothing tells you about: there is no write to call
153
+ `changed()` from, so poll it - `usePolling` - and keep `refreshOn` for data
154
+ your own code changes.
155
+
156
+ ## Testing what a page watches
157
+
158
+ `createTestApp` renders the page as it is deployed, and `watched()` says what
159
+ each region refreshes on for that url - `page` for the page's own, a
160
+ section's name for each section's:
161
+
162
+ ```ts title="tests/live.test.ts"
163
+ import { createTestApp } from '@rsc-kit/core/testing'
164
+
165
+ const app = await createTestApp()
166
+
167
+ test('the team page watches the team and its apps', async () => {
168
+ expect(await app.watched('/teams/acme')).toEqual({
169
+ page: ['team:acme'],
170
+ apps: ['team:acme:apps'],
171
+ })
172
+ })
173
+ ```
174
+
175
+ A region whose `refreshOn` gave no names is there with `[]` - and said in the
176
+ server's log in development, since that is usually a param read under the
177
+ wrong name. To check what a change does, say it - `changed()` in an app
178
+ that keeps its own versions, `testChanges()` with a test backend - and fetch
179
+ the section again: [What a page watches](/guides/testing#what-a-page-watches).
180
+
117
181
  ## How a tab finds out
118
182
 
119
183
  Each name has a version, a number that moves when something says the name
@@ -436,7 +500,8 @@ there is no single long-lived process:
436
500
  - **Every isolate or instance watches for its own tabs.** Versions in memory
437
501
  belong to one isolate, so a `changed()` handled by one never reaches tabs
438
502
  held by another. Use a shared store - D1, Postgres, your backend - which the
439
- production warning asks for if there is none.
503
+ production warning asks for if there is none. On Workers, [one hub](#one-hub-on-workers)
504
+ can hold every tab instead.
440
505
  - **Connections are cut.** Platforms bound how long a request may run; a
441
506
  stream that is cut reconnects on its own, and the tab catches up from its
442
507
  versions. On a platform that bills a function for every second it is open,
@@ -492,6 +557,78 @@ The Cloudflare example's `/live` page is all of this, end to end, and was run
492
557
  on `wrangler dev`: a change from its webhook reaches an open tab, and the
493
558
  section refreshes with the new value.
494
559
 
560
+ ### One hub on Workers
561
+
562
+ Each isolate watches only the tabs it happens to hold, and on Workers that is
563
+ often one. So by default every open tab costs its own ask of the store, every
564
+ two seconds, and a `wakeOn` listener runs once per isolate. A Durable Object
565
+ is one place every isolate can reach. Name it, and every tab's stream goes to
566
+ one instance, which watches all of them:
567
+
568
+ ```ts title="vite.config.ts"
569
+ // cloudflare_module, plus the Durable Object class the hub runs in.
570
+ nitro({ preset: 'cloudflare_durable', serveStatic: 'inline' })
571
+ ```
572
+
573
+ ```jsonc title="wrangler.jsonc"
574
+ {
575
+ "durable_objects": {
576
+ "bindings": [{ "name": "$DurableObject", "class_name": "$DurableObject" }]
577
+ },
578
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["$DurableObject"] }],
579
+ "vars": { "RSC_CHANGES_HUB": "$DurableObject" }
580
+ }
581
+ ```
582
+
583
+ Nothing changes in the app. `RSC_CHANGES_HUB` names the binding, and rsc-kit
584
+ does not look for one on its own: without it, each isolate watches its own
585
+ tabs as before. A name with no binding behind it is said once in the log, and
586
+ the isolate falls back to that.
587
+
588
+ What changes, against each isolate watching its own tabs:
589
+
590
+ | | Each isolate (default) | One hub |
591
+ | --- | --- | --- |
592
+ | Asks of the store or backend | one per open tab, every 2s | one for every tab, every 2s |
593
+ | A `changed()` reaches the tabs | at each isolate's next read, within a second or two | at once: `changed()` wakes the hub |
594
+ | A `wakeOn` listener or broadcast | one connection per isolate | one connection |
595
+ | Cost of a store with no LISTEN (D1) | grows with tabs | flat |
596
+ | A Laravel backend | one PHP request per tab, every 2s | one, for every tab |
597
+
598
+ - **Asks stop growing with tabs.** A thousand open tabs cost the store, or
599
+ your backend, one ask every two seconds instead of a thousand. On a backend
600
+ that answers each ask with a PHP request, this is the difference that
601
+ matters.
602
+ - **Changes arrive at once without LISTEN.** D1 cannot announce a change.
603
+ With the hub, `changed()` in any isolate - a webhook, an action - pokes the
604
+ hub after the write, and every tab hears it immediately instead of at the
605
+ next read.
606
+ - **One listener.** A broadcast server or `wakeOn` listener runs in the hub,
607
+ one connection for the deployment, rather than one per isolate that
608
+ happens to hold a tab.
609
+
610
+ What it costs, and its limits:
611
+
612
+ - **Durable Object time.** The hub stays active while any tab watches, and a
613
+ Durable Object is billed for the time it is active - for one instance open
614
+ all month, a few dollars on Cloudflare's paid plan. With no tab open it
615
+ costs nothing.
616
+ - **One location.** Every tab's stream goes to the one instance, wherever
617
+ Cloudflare placed it. An event crosses one more hop, tens of milliseconds;
618
+ the pages themselves are still rendered by the nearest isolate.
619
+ - **One instance's limits.** A Durable Object runs on one thread, and
620
+ Cloudflare documents a soft limit of about 1,000 requests a second for
621
+ one. A site whose tabs connect faster than that should stay on the
622
+ default flow, which spreads them across isolates.
623
+ - **Versions still live in a shared store.** The hub holds the tabs, not the
624
+ versions: D1, your database or your backend still keeps them, so a hub that
625
+ restarts loses nothing.
626
+
627
+ The forwarded requests carry a proof signed with `RSC_SIGNING_SECRET`, so a
628
+ browser cannot reach the hub's side of the endpoint directly. The Cloudflare
629
+ example runs this way, and was checked on `wrangler dev`: three tabs' streams
630
+ held by the hub, one webhook, and all three hearing the change.
631
+
495
632
  Where a response cannot stream at all, use `usePolling` instead.
496
633
 
497
634
  ## The table
@@ -643,17 +780,26 @@ random key per instance would fail silently.
643
780
  That refusal comes on the first request, because on some platforms - Workers
644
781
  - the environment is only there once a request arrives. Under Kubernetes or
645
782
  any orchestrator with a readiness check, that is too late: the pod is already
646
- taking traffic. Make the deploy itself fail instead, from `register()`, which
647
- runs before the server takes a request:
648
-
649
- ```ts title="src/instrumentation.ts"
650
- import { assertSigningSecret } from '@rsc-kit/core/changed'
783
+ taking traffic. Make the deploy itself fail instead, by declaring the secret
784
+ where every other variable is - in `env.ts`, which refuses a missing variable
785
+ before the server starts:
786
+
787
+ ```ts title="src/env.ts"
788
+ export const env = createEnv({
789
+ server: {
790
+ // Signs refreshOn's names. Required in production; in development a
791
+ // random key is fine, and what rsc-kit uses without one.
792
+ RSC_SIGNING_SECRET:
793
+ process.env.NODE_ENV === 'production' ? z.string().min(32) : z.string().optional(),
794
+ // ...
795
+ },
796
+ // ...
797
+ })
798
+ ```
651
799
 
652
- export function register() {
653
- assertSigningSecret() // throws in production without RSC_SIGNING_SECRET
654
- }
655
- ``` In development a random key is
656
- fine, and is what you get.
800
+ An app without an `env.ts` gets the same from `assertSigningSecret()`, from
801
+ `@rsc-kit/core/changed`, called in `register()`. In development a random key
802
+ is fine, and is what you get.
657
803
 
658
804
  How to generate it, where to keep it, and what rotating it does:
659
805
  [Secrets](/hosts/deployment#secrets).
@@ -674,7 +820,8 @@ by region, each time that changes:
674
820
  ```
675
821
 
676
822
  A region that declared `refreshOn` but is missing from that list rendered no
677
- names, and the server's log says why, once. And watching **stops while the
823
+ names, and the server's log says why, once - including when its function
824
+ returned none at all. And watching **stops while the
678
825
  tab is hidden** - a background tab holds no connection - which the console
679
826
  also says. Nothing is missed: shown again, the tab reconnects at the versions
680
827
  it holds and refreshes whatever moved meanwhile.
package/guides/mcp.md CHANGED
@@ -76,6 +76,7 @@ it is on the screen.
76
76
 
77
77
  | tool | answers |
78
78
  | --- | --- |
79
+ | `rules` | the rules for this app, as the installed version writes them - read first |
79
80
  | `list_topics` | every topic it can explain |
80
81
  | `how_to` | the short answer: forms, prefetch, validation, the action client, data with TanStack Query or SWR, Suspense, offline, pwa, no-javascript, api routes, authorization, why a page is dynamic |
81
82
  | `list_guides` | every guide on this site, one line each |
@@ -92,6 +93,43 @@ ways that compile either way** — a check written in the component instead of t
92
93
  action, a hand-parsed query string, `"use server"` at the top of a page. An
93
94
  agent that has not been told writes those confidently.
94
95
 
96
+ ## The rules an agent follows
97
+
98
+ `bun create rsc-kit` writes an `AGENTS.md`, and `rsc-kit init` adds a section
99
+ to the one you have. It opens with **Rules** - the things an agent gets wrong
100
+ in ways that compile:
101
+
102
+ - a change is not done without its test, and not until `check` passes;
103
+ - every environment variable is declared in `env.ts` (and `.env.example`) -
104
+ rsc-kit's own included;
105
+ - pages paint at once: each read is an async component under its own
106
+ `<Suspense>`, not a `loading.tsx`;
107
+ - server components call the data function directly, never the app's own api;
108
+ - live data is `refreshOn` with `changed()`, and polling only for what nothing
109
+ can announce;
110
+ - every action and query is built from the action client the scaffold writes
111
+ (`src/server/client.ts`), with authorisation as its `.use()` middleware - or,
112
+ with a Go or Laravel backend, in the backend's own guards;
113
+ - links and redirects are typed template literals; metadata is
114
+ `export const metadata`;
115
+ - generated files are never edited; secrets are generated, never committed;
116
+ - ask `how_to` before reaching for a pattern from another framework.
117
+
118
+ The section is rsc-kit's - between `<!-- rsc-kit:start -->` and
119
+ `<!-- rsc-kit:end -->`, stamped with the version that wrote it - and
120
+ everything around it is yours. When rsc-kit changes, bring it up to date:
121
+
122
+ ```sh
123
+ bunx rsc-kit agents # rewrites the rsc-kit section only; review it in git
124
+ ```
125
+
126
+ The dev server says when the section is older than the rsc-kit installed. An
127
+ `AGENTS.md` from before the stamp needs `--host` and `--source-dir` once; one
128
+ with no section at all is only rewritten with `--replace`. The `rules` tool
129
+ answers the same rules for the installed version, so an agent that reads it
130
+ first is never working from an outdated file. A Laravel app gets the same
131
+ rules from its Boost skill, in Laravel's terms.
132
+
95
133
  ## Two things worth knowing
96
134
 
97
135
  **Every answer says how old it is.** It reports the last build, and your files
package/guides/testing.md CHANGED
@@ -177,6 +177,29 @@ above. For the round trip itself, and for anything that depends on hydration —
177
177
  `useField` re-rendering, a navigation, an optimistic update reverting — use
178
178
  Playwright against `vite preview`, which serves the real build.
179
179
 
180
+ ## What a page watches
181
+
182
+ `watched()` renders a page and says what each region refreshes on - `page`
183
+ for the page's own `refreshOn`, a section's name for each section's - without
184
+ reading the signed names out of the payload:
185
+
186
+ ```ts
187
+ expect(await app.watched('/teams/acme')).toEqual({ page: ['team:acme'], apps: ['team:acme:apps'] })
188
+ ```
189
+
190
+ With a Go or Laravel backend stood in by `host`, `testChanges()` answers the
191
+ version questions the way the backend would, and its `changed` says a name
192
+ moved - what the backend's webhook would do:
193
+
194
+ ```ts
195
+ import { createTestApp, testChanges } from '@rsc-kit/core/testing'
196
+
197
+ const names = testChanges()
198
+ const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })
199
+
200
+ names.changed('team:1:repos')
201
+ ```
202
+
180
203
  ## What to test where
181
204
 
182
205
  | what | how |
@@ -186,5 +209,6 @@ Playwright against `vite preview`, which serves the real build.
186
209
  | an api route | call `GET`/`POST` with a `Request` |
187
210
  | anything that reads cookies or headers | `withRequest()` |
188
211
  | routing, guards, stored pages, status codes | `createTestApp().fetch()` |
212
+ | what a page refreshes on | `createTestApp().watched()` |
189
213
  | a client component | React + a DOM |
190
214
  | an action over the wire, hydration, navigation | Playwright |
@@ -66,6 +66,15 @@ run *as the visitor*: your session middleware reads the cookie and finds the
66
66
  same person the page is being rendered for. During a build there is no
67
67
  visitor and the headers are absent.
68
68
 
69
+ **Arguments that do not fit the function are a failure, not a refusal.**
70
+ Too few, or a value your language cannot decode into the parameter's type:
71
+ answer 500 with `error`, as Go and Laravel do. The renderer's calls are typed
72
+ from your own manifest, so a mismatch is a bug in a caller - most often a
73
+ manifest older than the code - and a 500 is what makes it show up in your
74
+ logs. Extra arguments may be ignored. Values that decode but break your
75
+ rules - an email that is not one - are the input being invalid: a 422 with
76
+ `validationErrors`, below.
77
+
69
78
  **Check the secret first, in constant time, and refuse before dispatch.** This
70
79
  endpoint runs functions by name with none of your routing in front of it;
71
80
  `hash_equals('', '')` is true in PHP and its equivalents elsewhere, so an
@@ -89,7 +98,7 @@ message to tell an invalid form from a broken server:
89
98
  | the input is invalid | 422 | `validationErrors: { "email": ["…"], "address.city": ["…"] }`, `error` |
90
99
  | there is no session | 401 | `unauthenticated: true`, `error` |
91
100
  | there is one, and still no | 403 | `unauthorized: true`, `error` |
92
- | go somewhere else | **200** | `redirect: "/login"`, optionally `redirectStatus` (default 307) |
101
+ | go somewhere else | **200** | `redirect: "/login"`, and `redirectStatus` when you mean something other than 307 |
93
102
  | refused, with its own status | that status | `error`, `refusalStatus: 429` (any 4xx or 5xx: a 503 while a dependency is down) |
94
103
  | the function failed | 500 | `error`, and in development `debug` |
95
104
 
@@ -100,7 +109,7 @@ and under `""` for a message about the form itself. `unauthenticated` and
100
109
  `ServerAuthorizationError`, so a page answers 401 or 403 the way it would
101
110
  have if a JavaScript guard had thrown them. `redirect` travels the path every
102
111
  other redirect travels — a real 3xx above a Suspense boundary, a digest below
103
- one. `revalidate` names [sections](/guides/sections) or `page`, and the answer
112
+ one. `revalidate` names [sections](/guides/live-data) or `page`, and the answer
104
113
  to the action carries the re-rendered region with it rather than the browser
105
114
  being told to ask again.
106
115
 
@@ -108,7 +117,10 @@ Two things that are easy to get wrong:
108
117
 
109
118
  **A redirect is a 200.** An HTTP client follows a 3xx transparently, so a real
110
119
  one here would send the host call itself to the destination and hand whatever
111
- it found back to the render as the function's result.
120
+ it found back to the render as the function's result. The redirect's own
121
+ status goes in `redirectStatus`; without it the renderer uses 307, which keeps
122
+ the method. Send it whenever your framework says something else - Laravel's
123
+ redirects are 302, so the Laravel package always sends it.
112
124
 
113
125
  **Refusing is not failing.** A form filled in wrongly is the ordinary case, and
114
126
  `validationErrors` is checked before `error` — a reply carrying both is read as
@@ -169,11 +181,19 @@ whole-batch form every call in it waits for the slowest.
169
181
 
170
182
  Either way, run every call and answer every one — a refusal in the second is
171
183
  that call's answer, not a reason to leave the third out. Each call's
172
- `revalidate` stays with that call. A backend that has not implemented
173
- batches at all loses nothing but the saving: the renderer reads its "no
174
- function name" answer as "no batches here" and sends single calls from then
175
- on. Batches never mix visitors; every call in one carried the same forwarded
176
- headers.
184
+ `revalidate` stays with that call. Batches never mix visitors; every call in
185
+ one carried the same forwarded headers.
186
+
187
+ **At most 50 calls.** The renderer never sends more in one batch; a page with
188
+ more is split. Refusing a larger one - 413, before running anything - is a
189
+ safe limit, and Laravel's.
190
+
191
+ **Batches are not optional.** The renderer always sends calls from the same
192
+ tick together, and an answer that is neither form - whatever its status -
193
+ fails every call in the batch with what your backend said. None is sent
194
+ again one by one: they may have run, and an action run twice is worse than
195
+ one reported as failed. The conformance suite checks batches, so a backend
196
+ that passes it takes them.
177
197
 
178
198
  ## Route middleware
179
199
 
@@ -222,7 +242,9 @@ an action. Every open tab showing it refreshes, with nothing polling. Two
222
242
  things to provide:
223
243
 
224
244
  **A way to say a name changed**, callable from anywhere in your backend. It
225
- moves the name's version: a number, kept somewhere every instance of your
245
+ moves the name's version to the larger of one past where it was and the
246
+ current time in milliseconds - so a version never comes round again, and an
247
+ old name can be deleted at any time - kept somewhere every instance of your
226
248
  backend shares - a cache, a table - since a webhook lands on whichever
227
249
  instance the balancer picked. A name never changed is at 0.
228
250
 
@@ -236,7 +258,11 @@ how long it would wait:
236
258
  Answer `{ "result": { "versions": { ... } } }` with every name in `since`
237
259
  whose version differs now, and nothing else - an empty object when none
238
260
  does. If you can, hold the call up to `wait` milliseconds for one to move
239
- and answer the moment it does: that is what makes the refresh immediate. If
261
+ and answer the moment it does: that is what makes the refresh immediate.
262
+ The renderer asks with 5 seconds; hold no longer than `wait`, and never past
263
+ 30 seconds, where the renderer gives up on any call. A name you have never
264
+ seen is at 0, compared like any other: held while the asker also holds 0,
265
+ answered the moment it is changed. If
240
266
  you cannot - PHP-FPM, a worker that must answer and go - answer at once; the
241
267
  renderer asks again on its interval, a couple of seconds apart. Both are
242
268
  conformant, and the page is the same.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/mcp",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.30.0",
37
+ "create-rsc-kit": "^0.29.0",
37
38
  "zod": "^3.25"
38
39
  },
39
40
  "devDependencies": {