solid2-agent-kit 0.8.0 → 0.9.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.
@@ -1059,7 +1059,11 @@ function doctorFindings(target) {
1059
1059
  for (const guidance of GUIDANCE_FILES) {
1060
1060
  const path = join(target, guidance);
1061
1061
  if (!existsSync(path)) continue;
1062
- const installed = readFileSync(path, 'utf8').match(/solid2-agent-kit v(\d[\w.-]*)/)?.[1];
1062
+ // The version must end on a word character so the sentence punctuation
1063
+ // that follows the marker ("… v0.8.0. Do not edit …", "…v0.8.0**.") is
1064
+ // not captured — with `[\w.-]*` alone the trailing period made every
1065
+ // freshly synced file compare as "0.8.0." !== "0.8.0" (always stale).
1066
+ const installed = readFileSync(path, 'utf8').match(/solid2-agent-kit v(\d(?:[\w.-]*\w)?)/)?.[1];
1063
1067
  if (installed && installed !== VERSION) {
1064
1068
  report(
1065
1069
  'stale-guidance',
@@ -17,8 +17,20 @@ Steps:
17
17
  - async computations reading reactive inputs **after** the first `await`;
18
18
  - effects that copy state instead of deriving it, or reading stores in the apply phase;
19
19
  - `<For>` over server/refetched rows without a stable-id key function;
20
- - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or async memos
21
- passed as values (`user={user()}`) instead of accessors;
20
+ - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
21
+ `const u = user()` extracted then passed (a real parent-side read). Passing
22
+ `user={user()}` is the colorless form — do not "fix" it into accessors or
23
+ `Promise<User>` props;
24
+ - `latest(selectedId)` as the default highlight (hold is the default);
25
+ `isPending` treated as a global spinner instead of a per-expression question;
26
+ - treating `<Errored>` as a terminal ErrorBoundary, or routing per-row mutation
27
+ failures through it (those belong in the action / a projection-folded map);
28
+ - nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
29
+ - rewriting App with loading/error branches, or snapshot/restore, when wrapping
30
+ a client store in server functions; disabling optimistic rows until ack;
31
+ - tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
32
+ refetch in `hydrate`/`onSettled`, a custom Worker/Express adapter, or
33
+ delaying `renderToStream` for visual order (`<Reveal>` owns display);
22
34
  - store updates that rebuild objects/arrays instead of mutating the draft or reconciling;
23
35
  - context values passed as snapshots instead of accessors/setters/stores;
24
36
  - components with conditional/early returns on reactive values.
@@ -5,4 +5,4 @@
5
5
  - After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
6
  - After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
7
7
  - The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
8
- - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state).
8
+ - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`.
@@ -75,20 +75,35 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
75
75
  array from the setter (`setTodos((t) => t.filter(...))`) *renders* but is unkeyed — same
76
76
  identity loss as wholesale assignment; mutate the draft or `reconcile`. Stores are
77
77
  **property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
78
- 10. **Async data is an async computation**: `createMemo(async () => ...)` read under
79
- `<Loading>` / `<Errored>` boundaries. Read every reactive input **before the first
78
+ 10. **Async data is an async computation**: `createMemo(async () => ...)` (or
79
+ `createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
80
+ **Fetch high, block low** — creating/reading are like sync; consuming (await)
81
+ and blocking (the boundary) are independent. Create the memo early, wrap only
82
+ the read with `<Loading>`. Do not glue the boundary to the memo, and do not
83
+ retarget intermediate props to `Promise<User>` / `Accessor<User>` — types stay
84
+ `User`. Nested trees do not suspend: they fetch in parallel; a real waterfall
85
+ looks like `fetchB(a().id)`. JSX component props are lazy (passing isn't reading):
86
+ `<Child user={user()} />` then `{props.user.name}` under the child's
87
+ `<Loading>` is the colorless form. `const u = user()` at the parent *is* a
88
+ read and throws/snapshots there. Pass the memo itself only when the child
89
+ must `refresh()` that source — `isPending(() => props.user)` works on the
90
+ value. Read every reactive input **before the first
80
91
  `await`** — post-`await` reads do not subscribe, and in production the computation can
81
92
  sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
82
93
  `<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
83
94
  during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
84
- accessor) only when that identity change should show the fallback again. Pass an async
85
- memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
86
- child's `<Loading>` — `user={user()}` throws not-ready at the parent, so the child's
87
- boundary never sees it. Do not start `fetch` (or any request) at component-body top
95
+ accessor) only when that identity change should show the fallback again. Do not start `fetch` (or any request) at component-body top
88
96
  level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
89
97
  use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
90
- `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer. `isPending`
91
- is the *refetch* indicator after a settled answer exists — do not use it as the
98
+ `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
99
+ Default navigation holds: keep `selectedId()` so highlight and content stay
100
+ consistent; `latest(selectedId)` only when the highlight should move first.
101
+ Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
102
+ `transition-delay` so fast swaps do not flash.
103
+ `isPending` is a per-expression question, not a global spinner: ask it of the
104
+ async source, a derived prop (`isPending(() => props.story)`), or the write
105
+ (`isPending(selectedId)`), even when the fetch lives in a child. It is
106
+ the *refetch* indicator after a settled answer exists — do not use it as the
92
107
  first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
93
108
  not `isPending(user())` (that evaluates the read before `isPending` runs).
94
109
  "Prop with local edits" = writable derivation:
@@ -143,8 +158,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
143
158
  integration layers. Model async as a computation (return a promise or async iterable
144
159
  from a memo/store) and let the framework own the states: first load = `<Loading>`
145
160
  fallback; refetch indicator = `isPending()`; errors = thrown into the graph and caught
146
- by `<Errored>`; speculative value = `latest()`. Mechanically enforced by
147
- `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
161
+ by `<Errored>`; speculative value = `latest()`. `<Errored>` is graph status, not a
162
+ terminal React ErrorBoundary: it heals when the source succeeds again (refresh,
163
+ live reconnect, input change). `reset` retries the collected *sources*, not a UI
164
+ remount. Per-row mutation failures belong in an errors map the projection folds
165
+ in (survives the optimistic overlay), not in `<Errored>` around the whole list.
166
+ Making a client store "real" is additive: same setters, wrap mutations in
167
+ `action`, swap `createOptimisticStore` and a file of server functions — do
168
+ not rewrite `App.tsx` with loading/error branches.
169
+ Mechanically enforced by `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
148
170
  19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
149
171
  counter/version signal read inside the computation. Input-driven refetch is automatic
150
172
  (tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
@@ -169,7 +191,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
169
191
  `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
170
192
  modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
171
193
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
172
- except reactive clients (e.g. Convex) whose subscriptions already push authoritative
194
+ the sync mutation *is* the prediction (an overlay, discarded on settle); put
195
+ `pending` on the record rather than a second copy of state. Do not snapshot
196
+ and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
197
+ unrelated writes because one action is in flight; failed-action replay is
198
+ optional. Except reactive clients (e.g. Convex) whose subscriptions already push authoritative
173
199
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
174
200
  Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
175
201
  after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
@@ -230,11 +256,32 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
230
256
  `<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
231
257
  `<title>` in the document shell is the fallback; do not hardcode other tags
232
258
  (`<meta name="description">`) that Solid Meta should manage.
233
- Return `respond(value, { status })` from a `"use server"` function, not
259
+ Return `respond(value, { status, headers })` from a `"use server"` function, not
234
260
  `Response.json` — a raw `Response` is HTTP-handler control flow; scripted
235
- callers should receive the value. JSON-encodable server-function arguments
261
+ callers should receive the value. Cache GET reads with `Cache-Control` on that
262
+ response, not a hand-rolled Solid cache. HTTP does not enforce TypeScript —
263
+ validate inside the function; do not invent tRPC / RPC type-gen.
264
+ Locals only the `"use server"` body reads (db, secrets) stay off the client.
265
+ Do not add an API-route file just to wrap a database call — `"use server"`
266
+ is the RPC. During SSR the call is in-process (no HTTP). After a mutation, `reload` /
267
+ `revalidate` / single-flight — never a client `fetch` or an `onSettled` /
268
+ `hydrate` refetch to "refresh the page". In-flight Promises serialize as
269
+ Promises; `live()` embeds the first value in HTML and continues the stream on
270
+ the client. Do not delay `renderToStream` for visual order (`<Reveal>` owns
271
+ display). Do not write subscribe/unsubscribe in the component around `live()`.
272
+ When the app has a server bundle (`ssr` or server functions), production is
273
+ `handleRequest(request)` from the built `dist/server/server.js` (or its
274
+ Fetchable `fetch`); platform Vite plugins adopt that handler (Node: the
275
+ template `server.js`). Client-only start mode is static `dist/client` —
276
+ there is no handler to wrap. Request middleware is
277
+ `start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
278
+ components from `"use server"` unless the project already enabled the
279
+ experimental `serverFunctions.components` flag. JSON-encodable server-function arguments
236
280
  only, unless `enableRichArguments()` was called once in the client entry
237
- (`Date` / `Map` / `Set` throw without it).
281
+ (`Date` / `Map` / `Set` throw without it). `live()` connection state is
282
+ `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
283
+ field in the yielded value. Sibling `<Loading>` reveal order is `<Reveal>`
284
+ (`collapsed` suppresses tail skeletons under sequential order).
238
285
 
239
286
  ## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
240
287
 
@@ -70,6 +70,8 @@ Most React `useEffect` code should NOT become `createEffect`:
70
70
  | Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
71
71
  | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; prefer over `<Dynamic>`) |
72
72
  | Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
73
+ | Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
74
+ | Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
73
75
  | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
74
76
  | Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
75
77
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
@@ -245,44 +247,106 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
245
247
  });
246
248
  ```
247
249
 
248
- ### Async data
250
+ ### Async data — fetch high, block low
251
+
252
+ Creating and reading an async value are the same as sync. **Consuming** (the
253
+ await) is a DX choice; **blocking** (the boundary) is a UX choice. Solid
254
+ unwelds them: where you create the memo is performance, where `<Loading>`
255
+ wraps the *read* is design, and neither is an architecture decision. JSX
256
+ component props are lazy (**passing isn't reading**), so intermediates do
257
+ not wait. Nested components mount immediately — they do not suspend — so
258
+ their fetches run in parallel. Making a value remote should touch the memo
259
+ and the boundary, not every file on the path, and should not change prop
260
+ types to `Promise<User>` or `Accessor<User>`.
249
261
 
250
262
  ```tsx
251
- import { Errored, For, Loading, createMemo, createSignal } from 'solid-js';
263
+ import { Errored, Loading, createMemo, createSignal, isPending } from 'solid-js';
252
264
 
253
- const [query, setQuery] = createSignal('');
254
- const results = createMemo(async () => {
255
- const value = query().trim();
256
- if (!value) return [];
257
- return api.search(value);
258
- });
265
+ function App() {
266
+ const [selectedId, setSelectedId] = createSignal(1);
267
+ const story = createMemo(() => fetchStory(selectedId()));
259
268
 
260
- return (
261
- <Errored fallback={(error) => <p>{String(error())}</p>}>
262
- <Loading fallback={<p>Searching...</p>}>
263
- <ul><For each={results()}>{(r) => <li>{r.title}</li>}</For></ul>
264
- </Loading>
265
- </Errored>
266
- );
269
+ return (
270
+ <Errored fallback={(error, reset) => <ErrorFallback error={error} reset={reset} />}>
271
+ <StoryList selectedId={selectedId()} onSelect={setSelectedId} />
272
+ <main class={{ pending: isPending(selectedId) }}>
273
+ <Loading fallback={<DetailSkeleton />}>
274
+ <StoryDetail story={story()} storyId={selectedId()} />
275
+ </Loading>
276
+ </main>
277
+ </Errored>
278
+ );
279
+ }
280
+
281
+ function StoryDetail(props: { story: Story; storyId: number }) {
282
+ const byline = createMemo(() => `${props.story.author} · ${props.story.points} points`);
283
+ return (
284
+ <article class={{ stale: isPending(() => props.story) }}>
285
+ <h1>{props.story.title}</h1>
286
+ <p>{byline()}</p>
287
+ <Comments storyId={props.storyId} />
288
+ </article>
289
+ );
290
+ }
291
+ ```
292
+
293
+ `story()` is a `Story`, not `Story | undefined` — if the computation is running,
294
+ the value is there. No `?.`, no `!`. `<StoryDetail story={story()} />` is the
295
+ correct colorless form: the prop expression is a getter, so `StoryDetail` (and
296
+ every layout in between) renders immediately. The wait happens when
297
+ `props.story.title` is read, under that `<Loading>`.
298
+
299
+ ```tsx
300
+ // WRONG — extracting at the parent is a real read; child's <Loading> never sees it
301
+ const current = story();
302
+ return <StoryDetail story={current} />;
303
+
304
+ // WRONG — Promise / Accessor types on the path. Making story remote is not a
305
+ // type change for StoryLayout / StoryDetail.
306
+ function StoryDetail(props: { story: Promise<Story> }) { /* ... */ }
307
+ function StoryDetail(props: { story: Accessor<Story> }) { /* ... */ }
308
+
309
+ // Fine — pass the memo itself only when the child must refresh() that source.
310
+ // isPending / latest are questions: they work on the value prop too.
311
+ return <StoryDetail story={story} />;
267
312
  ```
268
313
 
269
- - The `<Loading>` boundary must be an owner ancestor of the **read** (`results()`), not of
314
+ - The `<Loading>` boundary must be an owner ancestor of the **read**, not of
270
315
  where the memo was created, and not of page chrome (header/nav) that should stay mounted.
271
- Boundary placement is purely a UX decision — it does not change when fetches start
272
- (no waterfalls: nested components set up and fetch in parallel). After first paint the
316
+ Lifting the fetch does **not** mean lifting the boundary — that is consume glued
317
+ back onto block. After first paint the
273
318
  boundary keeps settled content during refetch; `isPending(results)` (pass the accessor,
274
- not `isPending(results())`) is the
275
- indicator. Use `on={query()}` (the **value**, not the accessor `on={query}`) only when
276
- that identity should put the fallback back on screen.
319
+ not `isPending(results())`) is the indicator. Use `on={query()}` (the **value**, not the
320
+ accessor `on={query}`) only when that identity should put the fallback back on screen.
277
321
  Do not use `isPending` as the first-load spinner.
322
+ - **Nesting is not a waterfall.** `Comments` above fetches in parallel with `story`
323
+ because it reads `props.storyId` (already known), not `props.story`. Passing
324
+ `storyId={props.story.id}` *would* wait on the story fetch — that is a real data
325
+ dependency, same as `createMemo(() => fetchAuthor(story().authorId))`. Components
326
+ do not suspend; they run once. Only the expressions that read wait, so a parent
327
+ JSX read does not delay a child that does not need that value.
328
+ - `isPending` is a **per-expression question**, not state and not a global spinner.
329
+ Ask it anywhere: on the async source (`isPending(results)`), below it
330
+ (`isPending(() => props.story)`), on a derived memo (`isPending(byline)`), or
331
+ *above* the fetch but below the write (`isPending(selectedId)`), even when the
332
+ fetch lives in a child. You do not need to pass the memo accessor just to ask.
333
+ - Default navigation **holds**. `selectedId()` and the old story stay put until
334
+ the new answer lands, so the highlight never points at the wrong content.
335
+ `latest(selectedId)` is the opt-in when the design wants the highlight to move
336
+ on click. Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
337
+ `transition-delay` so fast swaps do not flash.
278
338
  - Read every reactive input **before the first `await`** in an async computation. A read
279
339
  after `await` does not subscribe; production can sit pending with no retry.
280
340
  - `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
281
- `fallback={(error, reset) => ...}`.
341
+ `fallback={(error, reset) => ...}`. The boundary **heals** when the source succeeds
342
+ again (refresh, live reconnect, input change) — it is graph status, not a terminal
343
+ React ErrorBoundary. `reset` retries the collected *data sources*, not a UI remount.
282
344
  - Refetch: `refresh(results)`. In-flight indicator: `isPending(results)` (or
283
345
  `isPending(() => results())`). Freshest in-flight value for a preview:
284
346
  `latest(results)`. Do not start `fetch` at component-body top level.
285
- - Coordinate reveal order of sibling `<Loading>` boundaries with `<Reveal order="sequential" | "together" | "natural">`.
347
+ - Coordinate sibling `<Loading>` reveal with `<Reveal>` (`order="sequential"` default,
348
+ or `"together"` / `"natural"`). `collapsed` (sequential only) suppresses tail
349
+ skeletons past the frontier so the page does not stack fallbacks as popcorn.
286
350
 
287
351
  **Never hand-roll these states.** No `[loading, setLoading]` signal, no
288
352
  `data() === undefined` branch, no `{ data, error }` signal pair — those are React/Solid 1.x
@@ -334,21 +398,72 @@ function createSubscriptionQuery<T>(
334
398
  }
335
399
  ```
336
400
 
401
+ Server-function `live()` sources are the same shape (async iterable → memo). Connection
402
+ state is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), not a field
403
+ in the yielded value — see [Server functions](#server-functions--use-server).
404
+
337
405
  ### Mutations: `action` + optimistic state
338
406
 
407
+ The synchronous mutation *is* the prediction. Optimistic UI is not a second copy of
408
+ state — it is an overlay the graph discards when the action settles (pass or fail).
409
+ There is no rollback to write. Put in-flight affordances on the record
410
+ (`pending?: boolean`); they vanish with the overlay.
411
+
339
412
  ```tsx
340
413
  import { action, createOptimisticStore, refresh } from 'solid-js';
341
414
 
342
- const [todos, setTodos] = createOptimisticStore(fetchTodos, []);
343
-
344
- const addTodo = action(function* (todo: Todo) {
345
- setTodos((draft) => { draft.push(todo); }); // optimistic, auto-reverts on failure
346
- yield saveTodo(todo); // yield promises to stay in the transaction
347
- refresh(todos);
348
- });
349
- // invoke from an event handler: onClick={() => void addTodo(newTodo)}
415
+ type Todo = { id: string; title: string; completed: boolean; pending?: boolean; error?: { completed: boolean } };
416
+
417
+ function createTodos() { // call from a component — not module scope (SSR shares modules)
418
+ const errors = new Map<string, { completed: boolean }>();
419
+ const [todos, setTodos] = createOptimisticStore<Todo[]>(async () => {
420
+ const current = await api.list();
421
+ return current.map((todo) => {
422
+ const error = errors.get(todo.id);
423
+ return error ? { ...todo, error } : todo;
424
+ });
425
+ }, []);
426
+
427
+ const toggleTodo = action(function* (id: string, completed: boolean) {
428
+ setTodos((draft) => {
429
+ const todo = draft.find((row) => row.id === id);
430
+ if (!todo) return;
431
+ todo.completed = completed;
432
+ todo.pending = true;
433
+ });
434
+ try {
435
+ yield api.toggle(id, completed);
436
+ errors.delete(id);
437
+ } catch {
438
+ errors.set(id, { completed });
439
+ } finally {
440
+ refresh(todos);
441
+ }
442
+ });
443
+ return [todos, { toggleTodo }] as const;
444
+ }
350
445
  ```
351
446
 
447
+ Three layers, in this order: durable source → ephemeral UI that must survive
448
+ overlay discard (the `errors` map, folded in the projection) → optimistic overlay
449
+ (`pending`, the predicted `completed`). Consumers read one store. Catch
450
+ *expected* mutation failures in the action so a toggle error does not blank the
451
+ list through `<Errored>`; unhandled projection/render errors still reach the
452
+ boundary.
453
+
454
+ Going from a client store to the server is **additive**: same `setTodos`
455
+ calls, wrap each mutation in `action`, swap in `createOptimisticStore` and a
456
+ file of server functions. Do not rewrite `App.tsx` with loading/error branches,
457
+ and do not snapshot-the-cache / write-a-prediction / restore-on-error. A cache
458
+ library is not the productionizing step — the sync mutation already was the
459
+ prediction. (Router `query` or `@tanstack/solid-query` remain valid when the
460
+ project already uses them; they are not required to "make it real".)
461
+ Optimistic rows stay interactive — a not-yet-acked todo can still
462
+ be toggled. Do not disable the control until confirmation, and do not mutex
463
+ rapid clicks; `action` is the transaction (no half-mutation, no interleaved
464
+ list). An in-flight action does not freeze unrelated writes. Collecting failed
465
+ actions to replay is a design choice, not something the runtime requires.
466
+
352
467
  Ordinary signal/store writes inside an action are held until it settles. Never call
353
468
  `flush()` inside an action. Invoke actions from handlers, not from component/computation bodies.
354
469
  Do not set a `submitted` signal and watch it from an effect — that work belongs in the
@@ -356,11 +471,15 @@ handler or the `action`.
356
471
 
357
472
  `createOptimistic` / `createOptimisticStore` are for a tentative value during an
358
473
  active mutation, not a local editing session (use a writable derivation or a plain
359
- store for that).
474
+ store for that). A whole-form `createOptimistic(false)` saving flag is fine; prefer
475
+ `pending` on the record when the affordance is per item. The overlay is
476
+ discarded when the action settles — the confirmed store is the truth. Wrapping
477
+ the same store in server functions does not change that; do not add
478
+ snapshot/restore.
360
479
 
361
480
  **`yield`, not a bare `await`, is the transaction boundary.** `await api.save()` then
362
- `setTodos(...)` *runs*, but the write commits immediately and optimistic rollback is lost.
363
- Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
481
+ `setTodos(...)` *runs*, but the write commits immediately — it is not held as an
482
+ overlay. Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
364
483
 
365
484
  Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
366
485
  reads pull the async, and downstream async computeds hold previous values until the
@@ -593,6 +712,9 @@ DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and
593
712
  with `start: true` / `ssr: true` / `serverFunctions: true` only when the
594
713
  project already uses start mode. Tiers are `bare` → `basic` (router) →
595
714
  `fullstack` (SSR + server functions); do not jump a tier without cause.
715
+ Under `start: true` the plugin owns entries, the document shell, and the build —
716
+ write `src/App.tsx`, not SolidStart-style `entry-client.tsx` / `entry-server.tsx`
717
+ or a hand-written `index.html`.
596
718
 
597
719
  ### Solid Router 2 — `createRouter`, not JSX `<Route>`
598
720
 
@@ -702,11 +824,21 @@ export async function findUser(id: string) {
702
824
  ```
703
825
 
704
826
  A function-level server function cannot close over component locals — pass
705
- values as arguments. Treat every argument as untrusted. Read trusted identity
827
+ values as arguments. Treat every argument as untrusted — TypeScript does not
828
+ cross the HTTP boundary, so do not invent tRPC / RPC type-gen;
829
+ validate inside the function (a parse in the function body is that
830
+ validation, not an RPC schema across the wire). These functions *are* the RPC — do not add an
831
+ API-route file (or a Next `route.ts`) just to wrap `db.todos.insert`. API
832
+ routes are for real HTTP endpoints (webhooks, third-party POST), not for the
833
+ app's own RPC. Read trusted identity
706
834
  from `getRequestEvent()`, never from a caller-supplied user id. During SSR the same
707
- reference runs in-process; in the browser it is HTTP. Mutations that should
835
+ reference runs in-process (no HTTP); in the browser it is HTTP. Anything
836
+ referenced only inside the `"use server"` body (db client, secrets) never
837
+ reaches the client — the directive is the privacy boundary. Mutations that should
708
838
  revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
709
- guides). For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
839
+ guides). After a router/server mutation do **not** follow with a client `fetch`
840
+ to "refresh the page" — `reload` / `revalidate` (and the router's single-flight
841
+ response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
710
842
  `throw new Error("…")` is stripped to `"Internal Server Error"` in production —
711
843
  use `markSafeError` or `respond(..., { status })` for intentional client-facing
712
844
  failures. Do not `return Response.json(...)` from `"use server"` — that is
@@ -715,9 +847,65 @@ caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
715
847
  was called once in the client entry (`Date` / `Map` / `Set` throw without it).
716
848
  `GET()` is only for idempotent reads (URLs leak into logs/history);
717
849
  import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
850
+ Cache GET reads with HTTP, not a hand-rolled Solid cache: the caller still
851
+ receives the value; headers ride the transport.
852
+
853
+ ```ts
854
+ import { respond } from '@solidjs/web';
855
+ import { GET } from '@solidjs/web/server-functions';
856
+
857
+ export const fetchStory = GET(async (id: number) => {
858
+ 'use server';
859
+ const story = await db.stories.find(id);
860
+ return respond(story, {
861
+ headers: { 'cache-control': 'public, max-age=60, s-maxage=300' },
862
+ });
863
+ });
864
+ ```
865
+
718
866
  `live()` yields **current state** (each yield replaces the last); do not treat
719
- it as an append-only event log, and hoist the memo so consumers share one
720
- connection.
867
+ it as an append-only event log. Until the first yield it is unsettled
868
+ (`<Loading>`), same as a Promise; later yields are updates — the fallback does
869
+ not return.
870
+ Wire status is a side channel on the **iterable the `live()` call returns**,
871
+ not a field in the value and not a property of the memo:
872
+
873
+ ```ts
874
+ const source = stockPrice("ACME");
875
+ source.onstatus = (next) => setStatus(next);
876
+ const price = createMemo(() => source);
877
+ ```
878
+
879
+ Do not yield `{ price, connected: true }`. After the first successful connection,
880
+ transient failures retry with exponential backoff; a 4xx is a definite rejection.
881
+ There is no subscription API and no store wiring to configure. For a reactive
882
+ argument, call `live()` inside the memo so the input is tracked:
883
+ `createMemo(() => stockPrice(props.symbol))`. Every reader of that memo shares
884
+ one connection — hoist it instead of calling `live()` at each site. Do not
885
+ subscribe/unsubscribe in the component around `live()`. The `live()` generator
886
+ may still subscribe to an upstream source.
887
+
888
+ **The graph continues across the wire.** An in-flight Promise serializes as a
889
+ Promise and resolves in the client graph as it settles — including before the
890
+ bundle has loaded. A `live()` source sends its first value in the HTML then
891
+ picks up the stream. Do not refetch in `onSettled` / `hydrate` to "load for the
892
+ client", and do not delay `renderToStream` to preserve visual order — `<Reveal>`
893
+ controls when content *appears*, while HTML still ships as soon as it is ready.
894
+ With the router's single-flight integration, a mutation response can seed the
895
+ destination route's preloads; that is the refresh, not a follow-up `fetch`.
896
+ When the app has a server bundle (`ssr` or server functions), production is
897
+ `handleRequest(request)` from the built `dist/server/server.js` (or that
898
+ module's Fetchable default `fetch`) — not an import from `solid-js`. Cloudflare /
899
+ Netlify / Nitro Vite plugins adopt that handler — do not write a Solid adapter,
900
+ a custom Worker, or a Netlify Function. For Node, use the official template
901
+ `server.js` (web `Request` in, stream the response); do not invent an
902
+ Express/Solid bridge. Client-only `start: true` is still static `dist/client`
903
+ — there is no handler to wrap. Request middleware is the Vite option
904
+ `start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
905
+ Express `app.use`. Server functions that return components
906
+ (`serverFunctions: { components: true }`) are experimental preview — do not use
907
+ them unless the project already has that flag on.
908
+
721
909
  Unscripted forms: `<form method="post" action={createTodo.url}>` (the reference
722
910
  `.url`), not a client `preventDefault` + `fetch`, and not a hand-built
723
911
  `/_server/` URL. GET forms (`method="get" action={search.url}`) are only for
@@ -763,7 +951,7 @@ how to reuse. Prefer the form on the right.
763
951
  | `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
764
952
  | `setHandler(fn)` to store a function | `setHandler(() => fn)` (otherwise `fn` is an updater) |
765
953
  | `{user() ? <P user={user()!} /> : <SignIn />}` | `<Show when={user()}>{(u) => <P user={u()} />}</Show>` |
766
- | `<Child user={user()} />` for an async memo | `<Child user={user} />` and read `props.user()` under the child's `<Loading>` |
954
+ | `const u = user(); return <Child user={u} />` / `user: Promise<User>` / `user: Accessor<User>` as the default | `<Child user={user()} />` and `{props.user.name}` under `<Loading>` — types stay `User`. Pass the memo itself only when the child must `refresh()` that source. `isPending(() => props.user)` works on the value |
767
955
  | `lazy(() => import("./p").then((m) => ({ default: m.About })))` | `lazy(() => import("./p"), { export: "About" })` |
768
956
  | `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
769
957
  | `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
@@ -812,6 +1000,17 @@ how to reuse. Prefer the form on the right.
812
1000
  | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
813
1001
  | `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
814
1002
  | `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
1003
+ | `selectedId={latest(selectedId)}` as the default highlight | `selectedId={selectedId()}` so highlight and content stay consistent; `latest` only when the highlight should move first |
1004
+ | `isPending` on a root/global pending signal, or wrapping the whole shell | ask the question at the design site (dimmed list, disabled button, pane). It works on the source, a derived prop, or the write |
1005
+ | `<Loading>` glued to the `createMemo` (consume + block in one place) | fetch high, block low — the boundary wraps the read, not the creation |
1006
+ | `class={{ pending: isPending(id) }}` flashing on every navigation | same class + a short CSS `transition-delay` so only slow swaps show |
1007
+ | `<Comments />` nested under `{story().title}` assumed to waterfall | both mount immediately and fetch in parallel if the child reads an already-known id; a real waterfall is `createMemo(() => fetchAuthor(story().authorId))` or `storyId={props.story.id}` |
1008
+ | `<Errored>` as a terminal ErrorBoundary; `reset={() => location.reload()}` | the boundary heals when the source succeeds again; `reset` retries collected *sources*, not a UI remount |
1009
+ | `<Errored>` around a list catching a toggle/save failure | catch expected mutation failures in the `action`; keep row errors in a map the projection folds in (survives overlay discard) |
1010
+ | `yield { ...tick, connected: true }` from `live()` | yield the value; wire status is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`) |
1011
+ | sibling `<Loading>` fallbacks stacking as popcorn | `<Reveal collapsed>` (sequential default) suppresses tail skeletons past the frontier |
1012
+ | in-memory cache around a GET server function | `return respond(value, { headers: { "cache-control": "..." } })` — metadata rides HTTP, the value rides the graph |
1013
+ | `story()?.title` / treating `story()` as `Story \| undefined` | `{story().title}` under `<Loading>` — if the computation is running, the value is there |
815
1014
  | `typeof window !== "undefined"` | `isServer` / `isDev` from `@solidjs/web` |
816
1015
  | `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
817
1016
  | `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
@@ -837,6 +1036,16 @@ how to reuse. Prefer the form on the right.
837
1036
  | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
838
1037
  | `createEffect(() => source(), () => setError(null))` to reset a signal | `createSignal(() => { source(); return null; })` — the writable derivation resets on change; a sole-setter apply is rule 4 |
839
1038
  | a "server-safe" initial value adopted after mount (React hydration-mismatch fix) | client mode mounts with `render()` into an empty body — no hydration, no mismatch; read `localStorage` / `matchMedia` directly at signal creation |
1039
+ | rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap `createOptimisticStore` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
1040
+ | disable an optimistic row until ack, or mutex rapid clicks / freeze unrelated writes | keep the control enabled; `action` is the transaction. An in-flight action does not freeze other writes. Failed-action replay is optional |
1041
+ | tRPC / RPC type-gen around `"use server"` | TypeScript does not cross HTTP — validate inside the function. Locals only the body reads (db, secrets) stay off the client |
1042
+ | `src/routes/api/todos.ts` (or a Next `route.ts`) wrapping `db.todos.insert` | `"use server"` *is* the RPC. API routes are for real HTTP endpoints (webhooks), not the app's own mutations |
1043
+ | router/server mutation, then `fetch` / `onSettled` / `hydrate` to refresh the page | `reload` / `revalidate` / single-flight (the mutation response can seed destination preloads). In-flight Promises serialize as Promises; `live()` continues from HTML |
1044
+ | delay `renderToStream` so HTML arrives in visual order | stream as soon as ready; `<Reveal collapsed>` controls when content *appears* |
1045
+ | subscribe/unsubscribe in the component around `live()` | `createMemo(() => stockPrice(props.symbol))` — hoist that memo to share one connection. `onstatus` is on the iterable the `live()` call returns, not on the memo. Until the first yield, `<Loading>`; later yields are updates |
1046
+ | a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: template `server.js`. Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
1047
+ | `createMemo(async () => (await props.story).author)` | `createMemo(() => props.story.author)` — a derivation over an async value becomes async; no await, no Promise type |
1048
+ | return a component from `"use server"` / flip `serverFunctions.components` | experimental preview — do not enable unless the project already has that flag |
840
1049
 
841
1050
  ```tsx
842
1051
  // WRONG — post-await read never subscribes; production can hang pending
@@ -869,23 +1078,24 @@ function Field(props: { label: string; class?: string }) {
869
1078
  ```
870
1079
 
871
1080
  ```tsx
872
- // WRONG — user() throws not-ready at Page; child's <Loading> never sees it
1081
+ // WRONG — extracting at the parent is a real read; child's <Loading> never sees it
873
1082
  function Page() {
874
1083
  const user = createMemo(async () => (await fetch('/api/me')).json());
875
- return <Profile user={user()} />;
1084
+ const current = user();
1085
+ return <Profile user={current} />;
876
1086
  }
877
1087
 
878
- // CORRECT — pass the accessor; read it under the boundary that owns the fallback
879
- function Profile(props: { user: Accessor<User> }) {
1088
+ // CORRECT — JSX props are lazy (passing isn't reading); types stay User
1089
+ function Profile(props: { user: User }) {
880
1090
  return (
881
1091
  <Loading fallback={<p>Loading…</p>}>
882
- <h1>{props.user().name}</h1>
1092
+ <h1>{props.user.name}</h1>
883
1093
  </Loading>
884
1094
  );
885
1095
  }
886
1096
  function Page() {
887
1097
  const user = createMemo(async () => (await fetch('/api/me')).json());
888
- return <Profile user={user} />;
1098
+ return <Profile user={user()} />;
889
1099
  }
890
1100
  ```
891
1101
 
@@ -961,17 +1171,32 @@ always-applied rules installed alongside this skill.
961
1171
  an effect calling the setter.
962
1172
  - [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
963
1173
  and `onInput` for keystrokes. `innerHTML` is sanitized and not mixed with children.
964
- - [ ] Async memos are passed as accessors (`user={user}`), not `user={user()}`, so
965
- `<Loading>` around the *read* can catch not-ready.
1174
+ - [ ] Async memos are passed as values (`user={user()}`); JSX props are lazy.
1175
+ Types stay `User` — not `Promise<User>` / `Accessor<User>`. `<Loading>` wraps
1176
+ the *read*, not the memo, not chrome. Extracting `const u = user()` at the
1177
+ parent is the read that throws. Nested child fetches run in parallel
1178
+ (components do not suspend); a real waterfall is `fetchB(a().id)`.
1179
+ `isPending` is per-expression (`isPending(() => props.story)` works);
1180
+ `latest(selectedId)` is opt-in, not the default highlight.
966
1181
  - [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
967
1182
  ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
968
1183
  - [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
969
1184
  - [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
970
1185
  Reactive inputs of an async memo are read before the first `await`. No hand-rolled
971
1186
  `loading`/`error` signals or `=== undefined` readiness branches. First load is
972
- `<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`).
973
- Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
974
- different APIs — do not mix them.
1187
+ `<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`)
1188
+ or `isPending(selectedId)` when the write is the question. `<Errored>` heals
1189
+ when the source succeeds; `reset` retries sources, not a UI remount. Per-row
1190
+ mutation failures stay in the action/projection, not in `<Errored>` around
1191
+ the list. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
1192
+ different APIs — do not mix them. Client → server is additive (same
1193
+ setters + `action` + server functions): do not rewrite App with
1194
+ loading/error branches, disable optimistic rows until ack, or refetch in
1195
+ `hydrate` / `onSettled`. HTTP does not enforce TypeScript — validate
1196
+ inside `"use server"`; do not invent tRPC. `live()` owns the client connection.
1197
+ A server bundle uses `handleRequest` from `dist/server/server.js`; client-only start
1198
+ is static `dist/client`. Do not delay `renderToStream` for visual order
1199
+ (`<Reveal>` does).
975
1200
  - [ ] External collections reconcile into stores (function-form `createStore` /
976
1201
  `reconcile`), never wholesale draft assignment or `setTodos((t) => t.filter(...))`
977
1202
  as the identity-preserving update.
@@ -72,6 +72,16 @@ read these before writing route JSX.
72
72
  - https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-solid-router.md
73
73
  - https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-solid-meta.md
74
74
 
75
+ ## Design posts (Solid 2 async model)
76
+
77
+ Official Solid 2 blog series. Use these for *why* the colorless/async graph works;
78
+ verify API names against the reference pages above.
79
+
80
+ - https://www.solidjs.com/blog/solid-2-0-rc-the-big-reveal
81
+ - https://www.solidjs.com/blog/async-solid-fetch-high-block-low
82
+ - https://www.solidjs.com/blog/async-solid-write-sync-run-async
83
+ - https://www.solidjs.com/blog/async-solid-one-graph-two-machines
84
+
75
85
  ## Reference: reactivity (solid-js)
76
86
 
77
87
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-signal.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Teach AI coding agents (Cursor, Claude Code) to write correct Solid 2.0 instead of React or Solid 1.x — always-applied rules, an agent skill, and a mechanical pattern gate.",
5
5
  "type": "module",
6
6
  "license": "MIT",