@fougere/react 0.2.0-alpha.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/LICENSE +21 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/transport.d.ts +7 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +8 -0
- package/dist/transport.js.map +1 -0
- package/dist/useCurrentUser.d.ts +24 -0
- package/dist/useCurrentUser.d.ts.map +1 -0
- package/dist/useCurrentUser.js +36 -0
- package/dist/useCurrentUser.js.map +1 -0
- package/dist/useFormFor.d.ts +29 -0
- package/dist/useFormFor.d.ts.map +1 -0
- package/dist/useFormFor.js +78 -0
- package/dist/useFormFor.js.map +1 -0
- package/dist/useFougereData.d.ts +19 -0
- package/dist/useFougereData.d.ts.map +1 -0
- package/dist/useFougereData.js +85 -0
- package/dist/useFougereData.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fougere contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@fougere/react` — the four primitives, in React, for any host that renders it.
|
|
3
|
+
*
|
|
4
|
+
* It was extracted from `@fougere/next` once a second React host was on the table,
|
|
5
|
+
* and the extraction was cheap for a measurable reason: the hooks never imported
|
|
6
|
+
* Next. They reach `@fougere/app/client` and `react`, and talk to the server over
|
|
7
|
+
* `fetch` on same-origin paths — which is all a React app has in common with
|
|
8
|
+
* another React app.
|
|
9
|
+
*
|
|
10
|
+
* The Vue side has no counterpart on purpose. Nuxt's composables lean on
|
|
11
|
+
* `useAsyncData`, `useRequestFetch`, `useState` and `refreshNuxtData` — Nuxt's data
|
|
12
|
+
* layer, not Vue's — so a `@fougere/vue` would have to rebuild what Nuxt gives for
|
|
13
|
+
* free, and there is no second Vue host asking for it.
|
|
14
|
+
*/
|
|
15
|
+
export { useQuery, useCommand } from './useFougereData.js';
|
|
16
|
+
export { useFormFor, type FormOptions } from './useFormFor.js';
|
|
17
|
+
export { useCurrentUser, FougereSession } from './useCurrentUser.js';
|
|
18
|
+
export { fetcher, CALL_ENDPOINT } from './transport.js';
|
|
19
|
+
export type { CallInput, EntityClass, FormEntity, FormField, SessionView, } from '@fougere/app/client';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAExD,YAAY,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* `@fougere/react` — the four primitives, in React, for any host that renders it.
|
|
4
|
+
*
|
|
5
|
+
* It was extracted from `@fougere/next` once a second React host was on the table,
|
|
6
|
+
* and the extraction was cheap for a measurable reason: the hooks never imported
|
|
7
|
+
* Next. They reach `@fougere/app/client` and `react`, and talk to the server over
|
|
8
|
+
* `fetch` on same-origin paths — which is all a React app has in common with
|
|
9
|
+
* another React app.
|
|
10
|
+
*
|
|
11
|
+
* The Vue side has no counterpart on purpose. Nuxt's composables lean on
|
|
12
|
+
* `useAsyncData`, `useRequestFetch`, `useState` and `refreshNuxtData` — Nuxt's data
|
|
13
|
+
* layer, not Vue's — so a `@fougere/vue` would have to rebuild what Nuxt gives for
|
|
14
|
+
* free, and there is no second Vue host asking for it.
|
|
15
|
+
*/
|
|
16
|
+
export { useQuery, useCommand } from './useFougereData.js';
|
|
17
|
+
export { useFormFor } from './useFormFor.js';
|
|
18
|
+
export { useCurrentUser, FougereSession } from './useCurrentUser.js';
|
|
19
|
+
export { fetcher, CALL_ENDPOINT } from './transport.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAoB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kept as a re-export: the fetcher and the revalidation bus started here, and moving
|
|
3
|
+
* them into `@fougere/app/client` is what a second non-Nuxt client revealed — none
|
|
4
|
+
* of it was React. This file is the seam that used to hide that.
|
|
5
|
+
*/
|
|
6
|
+
export { fetcher, onRefetch, revalidate, CALL_ENDPOINT } from '@fougere/app/client';
|
|
7
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* Kept as a re-export: the fetcher and the revalidation bus started here, and moving
|
|
4
|
+
* them into `@fougere/app/client` is what a second non-Nuxt client revealed — none
|
|
5
|
+
* of it was React. This file is the seam that used to hide that.
|
|
6
|
+
*/
|
|
7
|
+
export { fetcher, onRefetch, revalidate, CALL_ENDPOINT } from '@fougere/app/client';
|
|
8
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session: resolved once server-side, handed down through the provider, read
|
|
3
|
+
* here as state. `refresh()` re-reads it after a client-side auth change (login,
|
|
4
|
+
* logout) — no page reload, no hand-rolled /api/me.
|
|
5
|
+
*
|
|
6
|
+
* `session` is the whole view (extensible context); `user` is its everyday
|
|
7
|
+
* projection. Nuxt hydrates through its own `useState` payload; React has no
|
|
8
|
+
* ambient equivalent, so a layout that already resolved the session passes it to
|
|
9
|
+
* `<FougereSession>` and the page arrives knowing its user. Without the provider
|
|
10
|
+
* the hook fetches once, which is correct and one round-trip slower.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from 'react';
|
|
13
|
+
import type { SessionView } from '@fougere/app/client';
|
|
14
|
+
export declare function FougereSession({ value, children }: {
|
|
15
|
+
value: SessionView;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}): import("react").FunctionComponentElement<import("react").ProviderProps<SessionView | null>>;
|
|
18
|
+
export declare function useCurrentUser<TUser = Record<string, unknown>>(): {
|
|
19
|
+
session: SessionView;
|
|
20
|
+
user: TUser | null;
|
|
21
|
+
loggedIn: boolean;
|
|
22
|
+
refresh: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=useCurrentUser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCurrentUser.d.ts","sourceRoot":"","sources":["../src/useCurrentUser.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AACH,OAAO,EAA8E,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+FAE9F;AAED,wBAAgB,cAAc,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;UAe1B,KAAK,GAAG,IAAI;;mBAXR,OAAO,CAAC,IAAI,CAAC;EAepD"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* The session: resolved once server-side, handed down through the provider, read
|
|
4
|
+
* here as state. `refresh()` re-reads it after a client-side auth change (login,
|
|
5
|
+
* logout) — no page reload, no hand-rolled /api/me.
|
|
6
|
+
*
|
|
7
|
+
* `session` is the whole view (extensible context); `user` is its everyday
|
|
8
|
+
* projection. Nuxt hydrates through its own `useState` payload; React has no
|
|
9
|
+
* ambient equivalent, so a layout that already resolved the session passes it to
|
|
10
|
+
* `<FougereSession>` and the page arrives knowing its user. Without the provider
|
|
11
|
+
* the hook fetches once, which is correct and one round-trip slower.
|
|
12
|
+
*/
|
|
13
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useState } from 'react';
|
|
14
|
+
const SessionContext = createContext(null);
|
|
15
|
+
export function FougereSession({ value, children }) {
|
|
16
|
+
return createElement(SessionContext.Provider, { value }, children);
|
|
17
|
+
}
|
|
18
|
+
export function useCurrentUser() {
|
|
19
|
+
const hydrated = useContext(SessionContext);
|
|
20
|
+
const [session, setSession] = useState(hydrated ?? { user: null });
|
|
21
|
+
const refresh = useCallback(async () => {
|
|
22
|
+
const response = await fetch('/_fougere/session');
|
|
23
|
+
setSession((await response.json()));
|
|
24
|
+
}, []);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!hydrated)
|
|
27
|
+
void refresh();
|
|
28
|
+
}, [hydrated, refresh]);
|
|
29
|
+
return {
|
|
30
|
+
session,
|
|
31
|
+
user: (session.user ?? null),
|
|
32
|
+
loggedIn: session.user != null,
|
|
33
|
+
refresh,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=useCurrentUser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCurrentUser.js","sourceRoot":"","sources":["../src/useCurrentUser.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;;;;;;;GAUG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAGnH,MAAM,cAAc,GAAG,aAAa,CAAqB,IAAI,CAAC,CAAC;AAE/D,MAAM,UAAU,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAA+C;IAC7F,OAAO,aAAa,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,QAAQ,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IAC5C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAc,QAAQ,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAEhF,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACpD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClD,UAAU,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAgB,CAAC,CAAC;IACrD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,QAAQ;YAAE,KAAK,OAAO,EAAE,CAAC;IAChC,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAExB,OAAO;QACL,OAAO;QACP,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAiB;QAC5C,QAAQ,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;QAC9B,OAAO;KACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FougereError } from '@fougere/core/contract';
|
|
2
|
+
import { type FormEntity, type FormField } from '@fougere/app/client';
|
|
3
|
+
export interface FormOptions {
|
|
4
|
+
/** Command the submit rides. Default: 'create'. */
|
|
5
|
+
op?: string;
|
|
6
|
+
/** Initial values (edit mode: the loaded entity). */
|
|
7
|
+
initial?: Record<string, unknown>;
|
|
8
|
+
/** Call params designating the target (edit mode: { id }). */
|
|
9
|
+
params?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function useFormFor<T = Record<string, unknown>>(entity: FormEntity, options?: FormOptions): {
|
|
12
|
+
fields: FormField[];
|
|
13
|
+
/**
|
|
14
|
+
* The same fields, keyed by name — a form that lays its inputs out by hand binds
|
|
15
|
+
* one at a time (`{...fieldsByName.email.attrs}`) and still states no rule of its
|
|
16
|
+
* own. Without it, a page retypes `type="email"` next to a card that already says
|
|
17
|
+
* `format: 'email'`, and the browser enforces the page rather than the declaration.
|
|
18
|
+
*/
|
|
19
|
+
fieldsByName: Record<string, FormField>;
|
|
20
|
+
values: Record<string, unknown>;
|
|
21
|
+
setValue: (name: string, value: unknown) => void;
|
|
22
|
+
errors: Record<string, string>;
|
|
23
|
+
submit: () => Promise<T | null>;
|
|
24
|
+
loading: boolean;
|
|
25
|
+
/** Non-validation failure of the last submit (unreachable host, conflict…). */
|
|
26
|
+
error: FougereError | null;
|
|
27
|
+
valid: boolean;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=useFormFor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFormFor.d.ts","sourceRoot":"","sources":["../src/useFormFor.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAa,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAML,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAgB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,WAAgB;;IAkDjG;;;;;OAKG;;;qBAzC+B,MAAM,SAAS,OAAO;;kBAgBrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;;IAgCpD,+EAA+E;;;EAIlF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* The form contract — state, validation, submission, error mapping. Never a
|
|
4
|
+
* widget: the page owns the rendering, this owns the mechanics.
|
|
5
|
+
*
|
|
6
|
+
* The cycle composes the other primitives: fields from the io axes, local
|
|
7
|
+
* pre-judgment with the same rules the handler enforces (one declaration, both
|
|
8
|
+
* sides), submission through the command (so the entity link revalidates mounted
|
|
9
|
+
* queries), and per-field errors in the same `{ path, message }` shape whoever
|
|
10
|
+
* judged.
|
|
11
|
+
*
|
|
12
|
+
* One visible difference from the Vue composable, and it is React's, not ours:
|
|
13
|
+
* Vue hands back a reactive object a template mutates in place, React state is
|
|
14
|
+
* replaced rather than mutated, so `setValue` is part of the contract here.
|
|
15
|
+
*/
|
|
16
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
17
|
+
import { FougereError, ErrorCode } from '@fougere/core/contract';
|
|
18
|
+
import { entityKeyOf, errorsByField, formFieldsOf, payloadOf, } from '@fougere/app/client';
|
|
19
|
+
import { useCommand } from './useFougereData.js';
|
|
20
|
+
export function useFormFor(entity, options = {}) {
|
|
21
|
+
const entityKey = entityKeyOf(entity);
|
|
22
|
+
const fields = useMemo(() => formFieldsOf(entity, entityKey), [entity, entityKey]);
|
|
23
|
+
// `initial` wins over the declared default: editing a row shows the row, including
|
|
24
|
+
// a value the author deliberately changed away from that default. On a create form
|
|
25
|
+
// there is no `initial`, so the field opens on what is about to be written — the
|
|
26
|
+
// schema's own literal, shown rather than guessed by the page.
|
|
27
|
+
const [values, setValues] = useState(() => Object.fromEntries(fields.map((field) => [field.name, options.initial?.[field.name] ?? field.default])));
|
|
28
|
+
const [errors, setErrors] = useState({});
|
|
29
|
+
const command = useCommand(entity, options.op ?? 'create');
|
|
30
|
+
const setValue = useCallback((name, value) => {
|
|
31
|
+
setValues((current) => ({ ...current, [name]: value }));
|
|
32
|
+
}, []);
|
|
33
|
+
/** Local pre-judgment — same rules as the handler, saves a lost round-trip. */
|
|
34
|
+
const judge = useCallback(() => {
|
|
35
|
+
const result = entity.validate(payloadOf(values));
|
|
36
|
+
setErrors(result.success ? {} : errorsByField(result.errors));
|
|
37
|
+
return result.success;
|
|
38
|
+
}, [entity, values]);
|
|
39
|
+
/**
|
|
40
|
+
* Judge locally, then send through the command. Returns the created/updated value,
|
|
41
|
+
* or null when a judge (either side) rejected — the errors land per field either
|
|
42
|
+
* way, the form never knows who judged.
|
|
43
|
+
*/
|
|
44
|
+
const submit = useCallback(async () => {
|
|
45
|
+
if (!judge())
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
return await command.execute({ params: options.params, body: payloadOf(values) });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof FougereError && err.code === ErrorCode.VALIDATION_FAILED && Array.isArray(err.details)) {
|
|
52
|
+
setErrors(errorsByField(err.details));
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}, [judge, command, options.params, values]);
|
|
58
|
+
const fieldsByName = useMemo(() => Object.fromEntries(fields.map((field) => [field.name, field])), [fields]);
|
|
59
|
+
return {
|
|
60
|
+
fields,
|
|
61
|
+
/**
|
|
62
|
+
* The same fields, keyed by name — a form that lays its inputs out by hand binds
|
|
63
|
+
* one at a time (`{...fieldsByName.email.attrs}`) and still states no rule of its
|
|
64
|
+
* own. Without it, a page retypes `type="email"` next to a card that already says
|
|
65
|
+
* `format: 'email'`, and the browser enforces the page rather than the declaration.
|
|
66
|
+
*/
|
|
67
|
+
fieldsByName,
|
|
68
|
+
values,
|
|
69
|
+
setValue,
|
|
70
|
+
errors,
|
|
71
|
+
submit,
|
|
72
|
+
loading: command.loading,
|
|
73
|
+
/** Non-validation failure of the last submit (unreachable host, conflict…). */
|
|
74
|
+
error: command.error,
|
|
75
|
+
valid: Object.keys(errors).length === 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=useFormFor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFormFor.js","sourceRoot":"","sources":["../src/useFormFor.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,WAAW,EACX,aAAa,EACb,YAAY,EACZ,SAAS,GAIV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAWjD,MAAM,UAAU,UAAU,CAA8B,MAAkB,EAAE,OAAO,GAAgB,EAAE;IACnG,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAEnF,mFAAmF;IACnF,mFAAmF;IACnF,iFAAiF;IACjF,+DAA+D;IAC/D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAA0B,GAAG,EAAE,CACjE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CACxG,CAAC;IACF,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAyB,EAAE,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,UAAU,CAAI,MAAqB,EAAE,OAAO,CAAC,EAAE,IAAI,QAAQ,CAAC,CAAC;IAE7E,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAAY,EAAE,KAAc,EAAE,EAAE;QAC5D,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC1D,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,+EAA+E;IAC/E,MAAM,KAAK,GAAG,WAAW,CAAC,GAAY,EAAE;QACtC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;QAClD,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAErB;;;;OAIG;IACH,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,IAAuB,EAAE;QACvD,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC1B,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACpF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,YAAY,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,iBAAiB,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1G,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,OAA8C,CAAC,CAAC,CAAC;gBAC7E,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE7C,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAA8B,EACjG,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,OAAO;QACL,MAAM;QACN;;;;;WAKG;QACH,YAAY;QACZ,MAAM;QACN,QAAQ;QACR,MAAM;QACN,MAAM;QACN,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,+EAA+E;QAC/E,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;KACxC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FougereError } from '@fougere/core/contract';
|
|
2
|
+
import { type CallInput, type EntityClass } from '@fougere/app/client';
|
|
3
|
+
export declare function useQuery<T = Record<string, unknown>>(entity: EntityClass, op: string, input?: CallInput, opts?: {
|
|
4
|
+
immediate?: boolean;
|
|
5
|
+
}): {
|
|
6
|
+
data: T | null;
|
|
7
|
+
items: T[];
|
|
8
|
+
total: number | undefined;
|
|
9
|
+
hasMore: boolean | undefined;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: FougereError | null;
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export declare function useCommand<T = unknown>(entity: EntityClass, op: string): {
|
|
15
|
+
execute: (input?: CallInput) => Promise<T>;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: FougereError | null;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=useFougereData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFougereData.d.ts","sourceRoot":"","sources":["../src/useFougereData.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAWL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAG7B,wBAAgB,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClD,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,MAAM,EACV,KAAK,CAAC,EAAE,SAAS,EACjB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;;;mBAYQ,OAAO,CAAC,IAAI,CAAC;EAiCpD;AAED,wBAAgB,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM;sBAMpD,SAAS,KAAG,OAAO,CAAC,CAAC,CAAC;;;EAqBxC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* The couple — useQuery (reads) and useCommand (writes), the two dual gestures of
|
|
4
|
+
* a page talking to a Frond, in React.
|
|
5
|
+
*
|
|
6
|
+
* What the gestures ARE lives in `@fougere/app/client` and is shared with Vue:
|
|
7
|
+
* designation is class + verb, the cache key derives from designation plus input,
|
|
8
|
+
* and a successful command on an entity revalidates every mounted query on that
|
|
9
|
+
* entity. Nothing of that is restated here — this file is `useState` for the
|
|
10
|
+
* values, `useEffect` for the mount, and the bus in `transport.ts` where Nuxt
|
|
11
|
+
* calls `refreshNuxtData`.
|
|
12
|
+
*/
|
|
13
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
14
|
+
import { asFougereError, callOf, entityKeyOf, invocationOf, itemsOf, mountedKeys, pageOf, queryKeyOf, sendCall, trackQuery, } from '@fougere/app/client';
|
|
15
|
+
import { fetcher, onRefetch, revalidate } from './transport.js';
|
|
16
|
+
export function useQuery(entity, op, input, opts) {
|
|
17
|
+
const entityKey = entityKeyOf(entity);
|
|
18
|
+
// The key IS the dependency: an input literal is a new object on every render, so
|
|
19
|
+
// depending on it directly would refetch forever. Its serialization is stable.
|
|
20
|
+
const key = queryKeyOf(entityKey, op, input);
|
|
21
|
+
const immediate = opts?.immediate !== false;
|
|
22
|
+
const [data, setData] = useState(null);
|
|
23
|
+
const [loading, setLoading] = useState(immediate);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const refresh = useCallback(async () => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
setData((await sendCall(fetcher, callOf(entity, op), invocationOf(input))));
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
setError(asFougereError(err, entityKey, op));
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
// `key` stands for (entity, op, input) — see above.
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
}, [key]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const untrack = trackQuery(entityKey, key);
|
|
42
|
+
const off = onRefetch(key, () => void refresh());
|
|
43
|
+
if (immediate)
|
|
44
|
+
void refresh();
|
|
45
|
+
return () => {
|
|
46
|
+
untrack();
|
|
47
|
+
off();
|
|
48
|
+
};
|
|
49
|
+
}, [entityKey, key, immediate, refresh]);
|
|
50
|
+
return {
|
|
51
|
+
data,
|
|
52
|
+
items: itemsOf(data),
|
|
53
|
+
total: pageOf(data).total,
|
|
54
|
+
hasMore: pageOf(data).hasMore,
|
|
55
|
+
loading,
|
|
56
|
+
error,
|
|
57
|
+
refresh,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function useCommand(entity, op) {
|
|
61
|
+
const entityKey = entityKeyOf(entity);
|
|
62
|
+
const [loading, setLoading] = useState(false);
|
|
63
|
+
const [error, setError] = useState(null);
|
|
64
|
+
const execute = useCallback(async (input) => {
|
|
65
|
+
setLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
const result = (await sendCall(fetcher, callOf(entity, op), invocationOf(input)));
|
|
69
|
+
// The link: same entity designated on both sides → revalidate its queries.
|
|
70
|
+
revalidate(mountedKeys(entityKey));
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const failure = asFougereError(err, entityKey, op);
|
|
75
|
+
setError(failure);
|
|
76
|
+
throw failure;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [entityKey, op]);
|
|
83
|
+
return { execute, loading, error };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=useFougereData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFougereData.js","sourceRoot":"","sources":["../src/useFougereData.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fougere/react",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "Fougere's four primitives as React hooks — host-independent.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fougere",
|
|
7
|
+
"react",
|
|
8
|
+
"hooks",
|
|
9
|
+
"typescript",
|
|
10
|
+
"fullstack",
|
|
11
|
+
"rpc"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/chok/fougere.git",
|
|
17
|
+
"directory": "packages/app/react"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@fougere/app": "0.2.0-alpha.0",
|
|
33
|
+
"@fougere/core": "0.2.0-alpha.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.2.2",
|
|
37
|
+
"react": "19.2.8",
|
|
38
|
+
"vitest": "^4.1.0",
|
|
39
|
+
"@testing-library/react": "^16.3.0",
|
|
40
|
+
"jsdom": "^27.0.0",
|
|
41
|
+
"react-dom": "19.2.8",
|
|
42
|
+
"@fougere/schema": "0.2.0-alpha.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "rm -rf dist && tsc",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
54
|
+
}
|
|
55
|
+
}
|