Merge branch 'v2/main' into v2/chore/2163-agent-skills
Brings in #2192 (react-hooks lint over `core/react/`) and #2194. All the code, `eslint.config.js` and the new root devDependency merged cleanly; three conflicts, all in files this PR refactored. - `.github/copilot-instructions.md` — modify/delete again. Kept deleted; the line v2/main added there restates AGENTS.md rules that arrive in AGENTS.md. - `AGENTS.md` — two of the three incoming hunks merged cleanly, both of them RULES, into the sections that survived the refactor: `exhaustive-deps` is `error` in every React scope including `core/react/**`, and `set-state-in-effect` now covers `core/react/` with `clients/tui` called out as the exception. The third was a `validate:core` bullet inside the pre-push gate section this PR moved wholesale to docs/quality-gate.md, so its mechanism went there instead — including why the plugin has to be a ROOT devDependency (Node resolution walks up, not down, so a client-local copy is unreachable). - `README.md` — the incoming paragraph was added to the quality-gate section this README no longer carries. Moved verbatim to docs/quality-gate.md, beside the type-aware-lint paragraph it belongs with. Ran `npm install` for the new devDependency; `npm run validate` passes. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
commit
d2a5e91e4a
15 changed files with 1042 additions and 85 deletions
|
|
@ -315,7 +315,7 @@ This exists because the gate's promise — that passing `npm run local:gate` loc
|
|||
Two consequences worth stating:
|
||||
|
||||
- **Do not silence a finding to satisfy the gate.** A warning is now a defect to fix. If a rule genuinely must be waived on a line, use its inline disable comment **with a one-line justification** — the same standard this document sets for `v8 ignore` and for `void` on a floating promise. Widening a `globalIgnores` or dropping a rule to make `lint` pass is not an acceptable fix.
|
||||
- **A rule left at `warn` still reads wrong in an editor.** The flag makes severity irrelevant to the *gate*, not to the developer looking at a squiggle. `react-hooks/exhaustive-deps` is therefore set to **`error`** in both React scopes (`clients/web`, `clients/tui`) rather than relying on the CLI flag alone. Prefer `error` for any rule you actually intend to enforce.
|
||||
- **A rule left at `warn` still reads wrong in an editor.** The flag makes severity irrelevant to the *gate*, not to the developer looking at a squiggle. `react-hooks/exhaustive-deps` is therefore set to **`error`** in every React scope (`clients/web`, `clients/tui`, and the root's `core/react/**` block) rather than relying on the CLI flag alone. Prefer `error` for any rule you actually intend to enforce.
|
||||
|
||||
## Typescript instructions
|
||||
|
||||
|
|
@ -405,7 +405,7 @@ Nothing **enforces** the boundary: no path alias keys off it, and the coverage `
|
|||
const MessageContainer = Paper.withProps({ variant: "message" });
|
||||
```
|
||||
- State and effects
|
||||
- **NEVER reset or re-sync local state from a prop inside a `useEffect`.** `useEffect(() => setX(prop), [prop])` renders once with the stale value, paints it, and only then corrects itself — the user sees the wrong frame and React renders twice. It is an error under `react-hooks/set-state-in-effect`, which the web client's `eslint-plugin-react-hooks` recommended set enforces.
|
||||
- **NEVER reset or re-sync local state from a prop inside a `useEffect`.** `useEffect(() => setX(prop), [prop])` renders once with the stale value, paints it, and only then corrects itself — the user sees the wrong frame and React renders twice. It is an error under `react-hooks/set-state-in-effect`, which the `eslint-plugin-react-hooks` recommended set enforces for the web client and — since #2192 — for `core/react/` too. (`clients/tui` registers the plugin but deliberately enables only `rules-of-hooks` and `exhaustive-deps`, for the reason its own config states.)
|
||||
- Use **`useValueChange(value, onChange)`** (`clients/web/src/hooks/useValueChange.ts`) instead. It is React's documented ["adjusting state during render"](https://react.dev/reference/react/useState#storing-information-from-previous-renders) pattern: it compares `value` against the previous render's with `Object.is` and calls `onChange(next)` during render, so React discards the in-progress output and re-runs the component before anything reaches the DOM. It does **not** fire on the first render — seed the dependent state with `useState` instead. Because the comparison is `Object.is`, the value you pass **must be referentially stable** across renders that mean "no change": prefer a primitive key derived from the data (an id, a name, a URI), and otherwise a memoized value. A fresh object/array literal would compare unequal every render and loop.
|
||||
- The `onChange` you pass runs **during render**, so it must be pure — `setState` calls and nothing else. No fetches, DOM writes, logging, ref mutation, or parent callbacks: a render can be replayed (StrictMode) or abandoned (concurrent React), so external work would run an unpredictable number of times.
|
||||
- An effect is still the right tool for genuine synchronization with an external system (DOM measurement, `requestAnimationFrame`, subscriptions, timers). The rule is about deriving React state from React props, not about effects in general. `NetworkEntry` shows the split: the reveal's force-open is a state update and uses `useValueChange`, while its `requestAnimationFrame` scroll stays a `useEffect`.
|
||||
|
|
|
|||
|
|
@ -51,17 +51,31 @@ interface Harness {
|
|||
* Hands the hook's API back to the test. The hook holds refs, so it has to be
|
||||
* read from inside a rendered component; `setServers` re-renders it with a new
|
||||
* list the way `useServers` does after a read.
|
||||
*
|
||||
* The returned object *delegates* rather than being the API from the first
|
||||
* render. `resolve` is keyed on the current `servers` (#2192) instead of on a
|
||||
* ref, so its identity changes with the list — handing back a snapshot would
|
||||
* give every test a `resolve` closed over the initial list, and `setServers`
|
||||
* would appear to do nothing.
|
||||
*/
|
||||
function harness(initial: ServerEntry[]): Harness {
|
||||
let api: LastPersistedSettings | undefined;
|
||||
let latest: LastPersistedSettings | undefined;
|
||||
function Probe({ servers }: { servers: ServerEntry[] }) {
|
||||
api = useLastPersistedSettings(servers);
|
||||
latest = useLastPersistedSettings(servers);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = renderWithMantine(<Probe servers={initial} />);
|
||||
if (!api) throw new Error("hook did not render");
|
||||
if (!latest) throw new Error("hook did not render");
|
||||
const current = () => {
|
||||
if (!latest) throw new Error("hook did not render");
|
||||
return latest;
|
||||
};
|
||||
return {
|
||||
api,
|
||||
api: {
|
||||
begin: (serverId) => current().begin(serverId),
|
||||
resolve: (serverId) => current().resolve(serverId),
|
||||
lastWriteFailed: (serverId) => current().lastWriteFailed(serverId),
|
||||
},
|
||||
setServers: (next) => rerender(<Probe servers={next} />),
|
||||
};
|
||||
}
|
||||
|
|
@ -222,4 +236,28 @@ describe("useLastPersistedSettings", () => {
|
|||
setServers([]);
|
||||
expect(api.resolve("A")).toBeUndefined();
|
||||
});
|
||||
it("answers from the list of the render it is called in", () => {
|
||||
// `useSettingsDraft` seeds its draft during render (#2192), so `resolve`
|
||||
// is called there. Reading the effect-mirrored `serversRef` would answer
|
||||
// from the *previous* commit's list on exactly the render where a
|
||||
// refreshed list arrives — which is the render that seeds the draft.
|
||||
const before = entry("A", settings({ paginatedLists: false }));
|
||||
const after = entry("A", settings({ paginatedLists: true }));
|
||||
const answers: Array<InspectorServerSettings | undefined> = [];
|
||||
|
||||
function Probe({ servers }: { servers: ServerEntry[] }) {
|
||||
const api = useLastPersistedSettings(servers);
|
||||
answers.push(api.resolve("A"));
|
||||
return null;
|
||||
}
|
||||
|
||||
const { rerender } = renderWithMantine(<Probe servers={[before]} />);
|
||||
expect(answers.at(-1)).toBe(before.settings);
|
||||
|
||||
answers.length = 0;
|
||||
rerender(<Probe servers={[after]} />);
|
||||
|
||||
expect(answers.length).toBeGreaterThan(0);
|
||||
for (const answer of answers) expect(answer).toBe(after.settings);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -145,16 +145,33 @@ export function useLastPersistedSettings(
|
|||
const outcomesRef = useRef<Map<string, { sequence: number; ok: boolean }>>(
|
||||
new Map(),
|
||||
);
|
||||
// Read the list through a ref so a write pairs with the entry as it stands
|
||||
// when it finishes, not the one captured when it was issued. Mirrored in an
|
||||
// effect rather than assigned during render: writing a ref mid-render is an
|
||||
// error under `react-hooks/refs`, and every reader runs from a settled
|
||||
// promise or an event handler, long after the commit.
|
||||
// `landed` reads the list through a ref so a write pairs with the entry as
|
||||
// it stands when it *finishes*, not the one captured when it was issued.
|
||||
// Mirrored in an effect rather than assigned during render: writing a ref
|
||||
// mid-render is an error under `react-hooks/refs`, and this reader runs from
|
||||
// a settled promise, long after the commit. `resolve` deliberately does not
|
||||
// use it — see there.
|
||||
const serversRef = useRef(servers);
|
||||
useEffect(() => {
|
||||
serversRef.current = servers;
|
||||
}, [servers]);
|
||||
|
||||
// Drop records the list has moved past. This used to happen lazily inside
|
||||
// `resolve`, which is no longer allowed: `useSettingsDraft` seeds its draft
|
||||
// during render (#2192) and so calls `resolve` there, where a ref mutation
|
||||
// would run an unpredictable number of times — React may replay a render
|
||||
// under StrictMode or abandon it altogether. Nothing depends on the eviction
|
||||
// happening at any particular moment; it only keeps a dead record from being
|
||||
// re-checked on every later write for that server, so an effect is the right
|
||||
// home for it.
|
||||
useEffect(() => {
|
||||
for (const [serverId, record] of recordsRef.current) {
|
||||
if (record.entry !== servers.find((s) => s.id === serverId)) {
|
||||
recordsRef.current.delete(serverId);
|
||||
}
|
||||
}
|
||||
}, [servers]);
|
||||
|
||||
const begin = useCallback((serverId: string): SettingsWrite => {
|
||||
const sequence = ++nextSequenceRef.current;
|
||||
const pending = pendingRef.current.get(serverId) ?? new Set<number>();
|
||||
|
|
@ -194,22 +211,25 @@ export function useLastPersistedSettings(
|
|||
[],
|
||||
);
|
||||
|
||||
// Pure, and over `servers` rather than `serversRef`. Both properties are
|
||||
// required because `useSettingsDraft` calls this during render to seed its
|
||||
// draft (#2192): a render must not mutate shared state, and the ref trails
|
||||
// the value it mirrors by one passive-effect flush, so reading it here would
|
||||
// answer from the previous commit's list on exactly the render where a
|
||||
// refreshed list arrives. Reading `servers` also makes the supersession test
|
||||
// fire a render earlier, which is the direction that matters — a list that
|
||||
// has genuinely been re-read is what the record is being checked against.
|
||||
const resolve = useCallback(
|
||||
(serverId: string): InspectorServerSettings | undefined => {
|
||||
const entry = serversRef.current.find((s) => s.id === serverId);
|
||||
const entry = servers.find((s) => s.id === serverId);
|
||||
const current = recordsRef.current.get(serverId);
|
||||
if (!current) return entry?.settings;
|
||||
// Identity, not deep equality: the question is whether the list has been
|
||||
// re-read since the write, not whether the values happen to match.
|
||||
if (current.entry !== entry) {
|
||||
// Superseded — drop it rather than re-checking a dead record on every
|
||||
// later write for this server.
|
||||
recordsRef.current.delete(serverId);
|
||||
return entry?.settings;
|
||||
}
|
||||
if (current.entry !== entry) return entry?.settings;
|
||||
return current.written;
|
||||
},
|
||||
[],
|
||||
[servers],
|
||||
);
|
||||
|
||||
return { begin, resolve, lastWriteFailed };
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
* persist, synchronous flush-on-close, and reset-on-close behaviors.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { act, render, renderHook } from "@testing-library/react";
|
||||
import { useEffect } from "react";
|
||||
import { useClientSettingsDraft } from "@inspector/core/react/useClientSettingsDraft";
|
||||
|
||||
interface SettingsShape {
|
||||
|
|
@ -422,4 +423,43 @@ describe("useClientSettingsDraft", () => {
|
|||
});
|
||||
expect(result.current.flush).toBe(flushAfterMount);
|
||||
});
|
||||
it("never commits the previous session's draft after reopening", () => {
|
||||
// Same reasoning as the `useSettingsDraft` counterpart: the regression is
|
||||
// a committed frame, not an end state. `rerender()` flushes the old
|
||||
// seeding effect before any assertion on `result.current`, so an
|
||||
// effect-seeded implementation passes those too. Record every commit from
|
||||
// inside the tree instead, and assert the previous session's draft is
|
||||
// never committed while the new one is open.
|
||||
const commits: Array<{ opened: boolean; text: string | null }> = [];
|
||||
let nextInitial: SettingsShape = { text: "first-session", rows: [] };
|
||||
|
||||
function Harness({ opened }: { opened: boolean }) {
|
||||
const { draft } = useClientSettingsDraft<SettingsShape>({
|
||||
opened,
|
||||
resolveInitial: () => nextInitial,
|
||||
onPersist: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
});
|
||||
useEffect(() => {
|
||||
commits.push({ opened, text: draft?.text ?? null });
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const { rerender } = render(<Harness opened={true} />);
|
||||
expect(commits).toContainEqual({ opened: true, text: "first-session" });
|
||||
|
||||
rerender(<Harness opened={false} />);
|
||||
|
||||
// Reopen against different saved config — the modal must never paint the
|
||||
// values from the session that just closed.
|
||||
nextInitial = { text: "second-session", rows: [] };
|
||||
commits.length = 0;
|
||||
rerender(<Harness opened={true} />);
|
||||
|
||||
expect(commits.length).toBeGreaterThan(0);
|
||||
for (const commit of commits) {
|
||||
expect(commit).toEqual({ opened: true, text: "second-session" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -189,4 +189,155 @@ describe("useEmaIdpLoginState", () => {
|
|||
).not.toHaveBeenCalled();
|
||||
expect(result.current.loginState).toBe("none");
|
||||
});
|
||||
it("a failed read for a new issuer reports none, not the old issuer's state", async () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600;
|
||||
const payload = btoa(JSON.stringify({ exp }))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
vi.mocked(storage.getIdpSession).mockResolvedValue({
|
||||
idToken: `h.${payload}.s`,
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ issuer }: { issuer: string }) =>
|
||||
useEmaIdpLoginState(storage, issuer, true),
|
||||
{ initialProps: { issuer: "https://idp.test" } },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loginState).toBe("logged_in");
|
||||
});
|
||||
|
||||
// The storage backend goes away *and* the issuer changes. The rejection
|
||||
// must not surface as an unhandled promise, and the new issuer must not
|
||||
// inherit the previous one's "logged_in" — nobody has authenticated
|
||||
// against it, and a read that never succeeded cannot say otherwise.
|
||||
const unhandled = vi.fn();
|
||||
process.on("unhandledRejection", unhandled);
|
||||
try {
|
||||
vi.mocked(storage.getIdpSession).mockRejectedValue(
|
||||
new Error("storage unreachable"),
|
||||
);
|
||||
await act(async () => {
|
||||
rerender({ issuer: "https://other-idp.test" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
} finally {
|
||||
process.off("unhandledRejection", unhandled);
|
||||
}
|
||||
|
||||
expect(storage.getIdpSession).toHaveBeenCalledWith(
|
||||
"https://other-idp.test",
|
||||
);
|
||||
expect(unhandled).not.toHaveBeenCalled();
|
||||
expect(result.current.loginState).toBe("none");
|
||||
});
|
||||
|
||||
it("keeps the last answer when a re-read of the same issuer fails", async () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600;
|
||||
const payload = btoa(JSON.stringify({ exp }))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
vi.mocked(storage.getIdpSession).mockResolvedValue({
|
||||
idToken: `h.${payload}.s`,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useEmaIdpLoginState(storage, "https://idp.test", true),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loginState).toBe("logged_in");
|
||||
});
|
||||
|
||||
// A transient storage outage is not evidence that the session ended, so
|
||||
// the issuer we are still looking at keeps the answer we actually got.
|
||||
vi.mocked(storage.getIdpSession).mockRejectedValue(
|
||||
new Error("storage unreachable"),
|
||||
);
|
||||
await act(async () => {
|
||||
await result.current.refresh().catch(() => {});
|
||||
});
|
||||
|
||||
expect(result.current.loginState).toBe("logged_in");
|
||||
});
|
||||
|
||||
it("ignores an overtaken read that resolves last for the same issuer", async () => {
|
||||
const jwt = (secondsFromNow: number) => {
|
||||
const payload = btoa(
|
||||
JSON.stringify({ exp: Math.floor(Date.now() / 1000) + secondsFromNow }),
|
||||
)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
return `h.${payload}.s`;
|
||||
};
|
||||
|
||||
// Two reads for the *same* issuer overlap, and the older one resolves
|
||||
// last. Issuer keying cannot tell them apart — both carry this issuer —
|
||||
// so only the read token stops the superseded answer from committing.
|
||||
let releaseFirst: () => void = () => {};
|
||||
const first = new Promise<{ idToken: string }>((resolve) => {
|
||||
releaseFirst = () => resolve({ idToken: jwt(3600) });
|
||||
});
|
||||
vi.mocked(storage.getIdpSession)
|
||||
.mockReturnValueOnce(first)
|
||||
.mockResolvedValue({ idToken: jwt(-3600) });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useEmaIdpLoginState(storage, "https://idp.test", true),
|
||||
);
|
||||
|
||||
// Second read starts and finishes while the first is still pending.
|
||||
await act(async () => {
|
||||
await result.current.refresh();
|
||||
});
|
||||
expect(result.current.loginState).toBe("expired");
|
||||
|
||||
await act(async () => {
|
||||
releaseFirst();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(result.current.loginState).toBe("expired");
|
||||
});
|
||||
|
||||
it("a logout supersedes a read that was already in flight", async () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600;
|
||||
const payload = btoa(JSON.stringify({ exp }))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
|
||||
let releaseRead: () => void = () => {};
|
||||
const pending = new Promise<{ idToken: string }>((resolve) => {
|
||||
releaseRead = () => resolve({ idToken: `h.${payload}.s` });
|
||||
});
|
||||
vi.mocked(storage.getIdpSession).mockReturnValue(pending);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useEmaIdpLoginState(storage, "https://idp.test", true),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.logout();
|
||||
});
|
||||
// Let the clear settle on its own before releasing the read, so the read
|
||||
// is unambiguously the *last* writer — without the token it would land on
|
||||
// top of the clear and report the cleared session as logged in.
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
expect(result.current.loginState).toBe("none");
|
||||
|
||||
await act(async () => {
|
||||
releaseRead();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(storage.clearIdpSession).toHaveBeenCalledWith("https://idp.test");
|
||||
expect(result.current.loginState).toBe("none");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
* appear to eat keystrokes. The PUT itself debounces.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { act, render, renderHook } from "@testing-library/react";
|
||||
import { useEffect } from "react";
|
||||
import { useSettingsDraft } from "@inspector/core/react/useSettingsDraft";
|
||||
|
||||
interface SettingsShape {
|
||||
|
|
@ -375,4 +376,46 @@ describe("useSettingsDraft", () => {
|
|||
});
|
||||
expect(result.current.flush).toBe(flushAfterMount);
|
||||
});
|
||||
|
||||
it("never commits the previous target's draft for a new target", () => {
|
||||
// The regression this pins is a *frame*, not an end state, so it cannot
|
||||
// be observed through `result.current`: RTL's `act` flushes passive
|
||||
// effects before the assertion runs, so an effect-seeded implementation
|
||||
// reaches the same final value and passes any check made after the
|
||||
// rerender settles. What distinguishes them is what was *committed* on
|
||||
// the way there — an effect renders once with the previous target's draft
|
||||
// and paints it before correcting itself.
|
||||
//
|
||||
// So record every commit from inside the tree and assert no committed
|
||||
// frame ever paired "beta" with alpha's draft.
|
||||
const commits: Array<{ targetId: string; text: string | null }> = [];
|
||||
const SETTINGS: Record<string, SettingsShape> = {
|
||||
alpha: { text: "alpha-value", rows: [] },
|
||||
beta: { text: "beta-value", rows: [] },
|
||||
};
|
||||
|
||||
function Harness({ targetId }: { targetId: string }) {
|
||||
const { draft } = useSettingsDraft<SettingsShape>({
|
||||
targetId,
|
||||
resolveInitial: (id) => SETTINGS[id] ?? EMPTY,
|
||||
onPersist: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
});
|
||||
useEffect(() => {
|
||||
commits.push({ targetId, text: draft?.text ?? null });
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const { rerender } = render(<Harness targetId="alpha" />);
|
||||
expect(commits).toContainEqual({ targetId: "alpha", text: "alpha-value" });
|
||||
|
||||
commits.length = 0;
|
||||
rerender(<Harness targetId="beta" />);
|
||||
|
||||
expect(commits.length).toBeGreaterThan(0);
|
||||
for (const commit of commits) {
|
||||
expect(commit).toEqual({ targetId: "beta", text: "beta-value" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,25 +33,33 @@ export function useClientSettingsDraft<T>({
|
|||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const latestValuesRef = useRef<T | null>(null);
|
||||
|
||||
const resolveInitialRef = useRef(resolveInitial);
|
||||
// Seeded during render rather than in an effect, for the reason spelled
|
||||
// out on {@link useSettingsDraft}: an effect would paint the previous
|
||||
// session's draft for one frame before correcting itself. `seeded` is
|
||||
// tracked separately from `draft` so a `resolveInitial` that legitimately
|
||||
// returns `null` is not re-seeded on every render.
|
||||
const [seeded, setSeeded] = useState(false);
|
||||
if (seeded !== opened) {
|
||||
setSeeded(opened);
|
||||
setDraft(opened ? resolveInitial() : null);
|
||||
}
|
||||
|
||||
const onPersistRef = useRef(onPersist);
|
||||
const onErrorRef = useRef(onError);
|
||||
const openedRef = useRef(opened);
|
||||
resolveInitialRef.current = resolveInitial;
|
||||
onPersistRef.current = onPersist;
|
||||
onErrorRef.current = onError;
|
||||
openedRef.current = opened;
|
||||
|
||||
// Written in an effect rather than during render (`react-hooks/refs`).
|
||||
// Every reader — the debounce timer, `flush`, and `onChange` — runs after
|
||||
// a commit, so a post-commit write is what they need. `latestValuesRef`
|
||||
// mirrors `draft` here so it is re-seeded (and cleared on close) without a
|
||||
// second copy of the seeding rule; `onChange` still writes it
|
||||
// synchronously, because a functional update in the same tick has to see
|
||||
// the value the previous call produced.
|
||||
useEffect(() => {
|
||||
if (!opened) {
|
||||
setDraft(null);
|
||||
latestValuesRef.current = null;
|
||||
return;
|
||||
}
|
||||
const initial = resolveInitialRef.current();
|
||||
setDraft(initial);
|
||||
latestValuesRef.current = initial;
|
||||
}, [opened]);
|
||||
onPersistRef.current = onPersist;
|
||||
onErrorRef.current = onError;
|
||||
openedRef.current = opened;
|
||||
latestValuesRef.current = draft;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { OAuthStorage } from "../auth/storage.js";
|
||||
import {
|
||||
clearEmaIdpSession,
|
||||
|
|
@ -13,6 +13,21 @@ export interface UseEmaIdpLoginStateResult {
|
|||
logout: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a completed read told us, and *which issuer* it was told about.
|
||||
*
|
||||
* The pair is stored together rather than as a bare `EmaIdpLoginState`
|
||||
* because a login state is only ever meaningful for the issuer it was read
|
||||
* for. With a bare value, changing issuer carries the previous issuer's
|
||||
* answer over to the new one until a fresh read lands — and if that read
|
||||
* fails, indefinitely, so the settings surface reports "logged in" for an
|
||||
* IdP nobody has authenticated against.
|
||||
*/
|
||||
interface ReadResult {
|
||||
issuer: string;
|
||||
value: EmaIdpLoginState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and clear EMA IdP session state for Client Settings UX.
|
||||
* Pass `active: true` while the settings surface is open to refresh on open.
|
||||
|
|
@ -23,31 +38,67 @@ export function useEmaIdpLoginState(
|
|||
active: boolean,
|
||||
): UseEmaIdpLoginStateResult {
|
||||
const normalizedIssuer = issuer ? normalizeIdpIssuer(issuer) : "";
|
||||
const [loginState, setLoginState] = useState<EmaIdpLoginState>("none");
|
||||
const [read, setRead] = useState<ReadResult>({ issuer: "", value: "none" });
|
||||
|
||||
/**
|
||||
* Monotonic read token — the same device `useInitialConfig` uses. Reads are
|
||||
* unordered, so an issuer flipped A → B → A can have the first A read
|
||||
* resolve last; a completion may commit only while it still holds the
|
||||
* current token. `logout` claims one too, so a read already in flight when
|
||||
* the session is cleared cannot land on top of the clear.
|
||||
*/
|
||||
const generation = useRef(0);
|
||||
|
||||
// Derived rather than stored: with no issuer there is nothing to be logged
|
||||
// in to, and a result read for some *other* issuer says nothing about this
|
||||
// one. A mismatch therefore reports "none" instead of the previous issuer's
|
||||
// answer, which is also what makes the failure path below safe.
|
||||
const loginState: EmaIdpLoginState =
|
||||
normalizedIssuer && read.issuer === normalizedIssuer ? read.value : "none";
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!normalizedIssuer) {
|
||||
setLoginState("none");
|
||||
return;
|
||||
}
|
||||
setLoginState(await getEmaIdpLoginState(storage, normalizedIssuer));
|
||||
const mine = ++generation.current;
|
||||
if (!normalizedIssuer) return;
|
||||
const value = await getEmaIdpLoginState(storage, normalizedIssuer);
|
||||
if (generation.current !== mine) return;
|
||||
setRead({ issuer: normalizedIssuer, value });
|
||||
}, [storage, normalizedIssuer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
void refresh();
|
||||
// Reading the persisted IdP session *is* the external system this effect
|
||||
// exists to synchronize with, and no commit happens here: `refresh`
|
||||
// returns before its first `await` when there is no issuer, and otherwise
|
||||
// commits in a continuation. That is also why this needs no
|
||||
// `set-state-in-effect` suppression, where `useServers` and
|
||||
// `useInitialConfig` do — deriving `loginState` from an issuer-keyed read
|
||||
// took the last synchronous setState out of the path.
|
||||
refresh().catch(() => {
|
||||
// `refresh` does not own its failures: `getEmaIdpLoginState` awaits
|
||||
// `storage.getIdpSession`, which rejects when the backend is
|
||||
// unreachable. Swallow it here so it does not surface as an unhandled
|
||||
// rejection, and commit nothing — the issuer keying above is what makes
|
||||
// that safe. A failed read for a *new* issuer reports "none", because
|
||||
// no result for that issuer exists; a failed re-read of the issuer
|
||||
// already on screen keeps the last answer we actually got, which is the
|
||||
// right reading of a transient storage outage.
|
||||
});
|
||||
}, [active, refresh]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
if (!normalizedIssuer) return;
|
||||
// Supersede any read in flight: it was started before the session was
|
||||
// cleared, so its answer is already wrong by the time it resolves.
|
||||
const mine = ++generation.current;
|
||||
void clearEmaIdpSession(storage, normalizedIssuer)
|
||||
.then(() => {
|
||||
setLoginState("none");
|
||||
if (generation.current !== mine) return;
|
||||
setRead({ issuer: normalizedIssuer, value: "none" });
|
||||
})
|
||||
.catch(() => {
|
||||
// Clearing the persisted IdP session failed (e.g. the storage backend
|
||||
// is unreachable). Swallow the rejection so it does not surface as an
|
||||
// unhandled promise, and leave loginState unchanged so the UI keeps
|
||||
// unhandled promise, and leave the last read in place so the UI keeps
|
||||
// reflecting the still-present session rather than falsely showing
|
||||
// signed-out.
|
||||
});
|
||||
|
|
|
|||
|
|
@ -245,11 +245,25 @@ export function useInitialConfig(
|
|||
}, [base, authToken, doFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
// `load` commits nothing before its first `await`: it claims a generation
|
||||
// token, builds headers, and only then fetches, so every setState it makes
|
||||
// runs in a continuation. `set-state-in-effect` follows the call into an
|
||||
// async function without modelling the `await` boundary, so it reports
|
||||
// this even though there is no synchronous commit to cascade from.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- no commit before the first await
|
||||
void load();
|
||||
return () => {
|
||||
// Invalidate whatever is in flight: this fires on unmount and on a
|
||||
// `baseUrl`/`authToken` change, and in both cases a response for the
|
||||
// old inputs must not commit.
|
||||
//
|
||||
// `generation` is a monotonic request token, not a ref to a rendered
|
||||
// node. `exhaustive-deps`' advice — copy `generation.current` into a
|
||||
// variable inside the effect and use the copy here — is exactly wrong:
|
||||
// the cleanup must bump whatever the counter holds *at teardown*, since
|
||||
// that is what invalidates the load currently in flight. A value
|
||||
// captured at effect setup would let a superseded response commit.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- must read the token at teardown
|
||||
generation.current++;
|
||||
};
|
||||
}, [load]);
|
||||
|
|
|
|||
|
|
@ -231,6 +231,14 @@ export function useServers(opts: UseServersOptions): UseServersResult {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Loading the server list from the backend is the external-system
|
||||
// synchronization this effect is for, and every list/error commit happens
|
||||
// after `loadServers()` resolves. `set-state-in-effect` follows the call
|
||||
// without modelling the `await`, so it reports the whole shape. The
|
||||
// synchronous part is the pair that *opens* a load — `setLoading(true)`
|
||||
// and clearing a stale error — which settles in one extra render, and is
|
||||
// the point of having a loading flag at all.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- async load of an external list
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ export interface UseSettingsDraftOptions<T> {
|
|||
targetId: string | undefined;
|
||||
/**
|
||||
* Resolves the initial draft for a given server id at the moment the
|
||||
* modal opens. Called once per target — must be stable enough that
|
||||
* the caller doesn't accidentally provoke re-initialization while
|
||||
* the user is editing. (We do not include this function in the
|
||||
* effect deps; it's read through a ref so callers don't have to
|
||||
* `useCallback` it.)
|
||||
* modal opens. Called once per target, **during render** — so it must
|
||||
* be pure: a render can be replayed under StrictMode or abandoned by
|
||||
* concurrent React, which would run any side effect an unpredictable
|
||||
* number of times. Its identity is never a dependency of anything, so
|
||||
* callers do not have to `useCallback` it, and re-creating it on every
|
||||
* render cannot re-seed an in-progress edit.
|
||||
*/
|
||||
resolveInitial: (id: string) => T;
|
||||
/** Persist the draft. Called from the debounced flush and from `flush`. */
|
||||
|
|
@ -73,31 +74,41 @@ export function useSettingsDraft<T>({
|
|||
const [draft, setDraft] = useState<T | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
// Seeding happens during render, not in an effect. An effect keyed on
|
||||
// `targetId` renders once with the *previous* target's draft, paints
|
||||
// it, and only then corrects itself — so opening the modal on a second
|
||||
// server briefly shows the first one's values. React's documented
|
||||
// "adjusting state when a prop changes" pattern discards the
|
||||
// in-progress render instead, so nothing stale reaches the DOM.
|
||||
// (`clients/web` spells the same thing `useValueChange`; `core/` cannot
|
||||
// import from a client.)
|
||||
//
|
||||
// `seededFor` is compared rather than `draft`, because "already seeded
|
||||
// for this target" and "the draft happens to be null" are different
|
||||
// states: a caller whose `resolveInitial` legitimately yields `null`
|
||||
// would otherwise be re-seeded on every render.
|
||||
const [seededFor, setSeededFor] = useState<string | undefined>(undefined);
|
||||
if (seededFor !== targetId) {
|
||||
setSeededFor(targetId);
|
||||
setDraft(targetId ? resolveInitial(targetId) : null);
|
||||
}
|
||||
|
||||
// Read callbacks (and the current draft) through refs so the consumer
|
||||
// doesn't have to `useCallback` them, and so the returned `flush`
|
||||
// identity is stable across keystrokes. The effect that
|
||||
// re-initializes the draft must depend on `targetId` only — anything
|
||||
// else in its deps risks resetting an in-progress edit when the
|
||||
// parent re-renders for unrelated reasons (e.g. a background
|
||||
// server-list refresh).
|
||||
const resolveInitialRef = useRef(resolveInitial);
|
||||
// identity is stable across keystrokes. Written in an effect rather
|
||||
// than during render (`react-hooks/refs`): a render can be replayed or
|
||||
// abandoned, and every reader here — the debounce timer and `flush` —
|
||||
// runs after a commit, so a post-commit write is what they need.
|
||||
const onPersistRef = useRef(onPersist);
|
||||
const onErrorRef = useRef(onError);
|
||||
const draftRef = useRef<T | null>(null);
|
||||
const targetIdRef = useRef(targetId);
|
||||
resolveInitialRef.current = resolveInitial;
|
||||
onPersistRef.current = onPersist;
|
||||
onErrorRef.current = onError;
|
||||
draftRef.current = draft;
|
||||
targetIdRef.current = targetId;
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetId) {
|
||||
setDraft(null);
|
||||
return;
|
||||
}
|
||||
setDraft(resolveInitialRef.current(targetId));
|
||||
}, [targetId]);
|
||||
onPersistRef.current = onPersist;
|
||||
onErrorRef.current = onError;
|
||||
draftRef.current = draft;
|
||||
targetIdRef.current = targetId;
|
||||
});
|
||||
|
||||
// Unmount cleanup — a stale timer firing after unmount is harmless
|
||||
// today (the persist call still goes through to the backend) but
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ That is the readable half, and prose rots. The enforced half is `scripts/lib/wor
|
|||
|
||||
| Script | What it does |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run validate` | Runs the four durable guards first — `verify:format-coverage` (every tracked source file is format-gated), `verify:skills` (every `.claude/skills` manifest parses and declares its invocation mode; the **model-invoked** ones also carry eval cases — the name-only skills intentionally have none), `verify:typecheck-coverage` (every tracked TS file lands in a tsconfig project), `verify:dep-lockstep` (no dependency reaching one `tsc` program from two installs skews across them) — then `test:scripts` (the guards' own parser unit tests), then `validate:core` (the shared `core/` `format:check` + `lint` gate), then per client: `format:check` + `lint` + **`typecheck`** (cli/tui/launcher; web typechecks via `tsc -b` inside its `build`) + `build` + fast unit tests. The quick inner-loop check. |
|
||||
| `npm run validate` | Runs the four durable guards first — `verify:format-coverage` (every tracked source file is format-gated), `verify:skills` (every `.claude/skills` manifest parses and declares its invocation mode; the **model-invoked** ones also carry eval cases — the name-only skills intentionally have none), `verify:typecheck-coverage` (every tracked TS file lands in a tsconfig project), `verify:dep-lockstep` (no dependency reaching one `tsc` program from two installs skews across them) — then `test:scripts` (the guards' own parser unit tests), then `validate:core` (the shared `core/` `format:check` + `lint` gate — including the `core/react/**` React-hooks block added by #2192, whose plugin is a **root** devDependency because a client-local copy is unreachable from the root: Node resolution walks up, not down), then per client: `format:check` + `lint` + **`typecheck`** (cli/tui/launcher; web typechecks via `tsc -b` inside its `build`) + `build` + fast unit tests. The quick inner-loop check. |
|
||||
| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project and covers the shared `core/` runtime (including `core/json` and `core/client`). |
|
||||
| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web — the TUI one runs under a **pseudoterminal** and asserts the Ink app is still running two seconds after its first frame, not merely that it painted one, [#2147](https://github.com/modelcontextprotocol/inspector/issues/2147)), plus three headless-browser smokes: a boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error — sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests), and an **MCP Apps** smoke (`smoke:web:app`) that drives connect → open app → `data-app-status="ready"` against a composable App server, covering the sandbox proxy and UI-protocol bridge, and an **app-rendered elicitation** smoke (`smoke:web:elicit`) that drives one end to end — call the tool, answer inside the sandboxed app, see the app's `ElicitResult` reach the server — and then the same tool against a server that never advertised the capability, which must fall back to the native elicitation form. Those three take their engine from `SMOKE_BROWSER` — see [Supported browsers](#supported-browsers). A fourth, **`smoke:web:tabs`**, drives the core tabs against one server on one browser launch — Tools runs a tool and asserts its `structuredContent` section, Resources reads a resource with templates listed, Prompts fetches one ([#2148](https://github.com/modelcontextprotocol/inspector/issues/2148)) — asserting the `data-*` contract in [the web README](../clients/web/README.md#core-tab-automation-contract) rather than visible copy. It is **Chromium-only**: those tabs are ordinary React and Mantine, so unlike the sandbox they are not engine-sensitive. |
|
||||
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run local:gate` and of GitHub CI. |
|
||||
|
|
@ -47,6 +47,8 @@ Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …),
|
|||
|
||||
**Linting is type-aware.** All five ESLint scopes (`clients/{web,cli,tui,launcher}` plus the root `core/` + shared gate) enable `@typescript-eslint/no-floating-promises` at `error`, so a promise that is neither awaited, returned, `.catch(…)`-terminated, nor explicitly discarded with `void` fails `lint` — and therefore `validate` ([#1959](https://github.com/modelcontextprotocol/inspector/issues/1959)). The rule needs type information, so each scope's config names a parser project; the root scope's is **`tsconfig.lint.json`**, a lint-only project covering `core/**`, `test-servers/src/**`, and `vitest.shared.mts`, which have no tsconfig of their own. It emits nothing and changes no typecheck — but a new first-party TS location added to the root lint scope must be added to its `include`. See **TypeScript instructions** in [`AGENTS.md`](../AGENTS.md) for when `void` is acceptable.
|
||||
|
||||
**The React hook rules cover `core/react/` too.** Every copy of `eslint-plugin-react-hooks` used to be client-local — a devDependency of `clients/web` and of `clients/tui`, each reachable only from that client's own `eslint .` — so the whole hook rule set, `set-state-in-effect` included, had never looked at `core/react/`, a directory of nothing but React hooks ([#2192](https://github.com/modelcontextprotocol/inspector/issues/2192)). "Neither surface has JSX" was the stated reason and it was never a good one: the rules judge hooks, not JSX. The plugin is now a root devDependency as well, applied by a `core/react/**` block in the root `eslint.config.js` — scoped there rather than to all of `core/**`, since the rules key off the `use`-prefix convention and a non-React `useFoo` helper elsewhere would be judged as a hook it is not.
|
||||
|
||||
**And lint has no warning tier.** Every `lint` script runs with `--max-warnings 0`, so a warning fails `validate` exactly as an error does ([#2085](https://github.com/modelcontextprotocol/inspector/issues/2085)) — a `warn`-level `react-hooks/exhaustive-deps` finding had otherwise let a stale-closure bug pass the mandatory pre-push gate and reach review. Fix the finding rather than silencing it; if a rule genuinely must be waived, use its inline disable comment with a one-line justification.
|
||||
|
||||
For the full testing rules — the ≥90% per-file gate, where test files live, the unit vs. integration vs. storybook projects, and the `v8 ignore` policy — see [`AGENTS.md`](../AGENTS.md).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
|
||||
|
|
@ -10,22 +11,26 @@ import { defineConfig, globalIgnores } from "eslint/config";
|
|||
// isomorphic TypeScript (browser-side OAuth + Node backends + shared runtime);
|
||||
// the shared surface is Node-only tooling/tests.
|
||||
//
|
||||
// ⚠️ There is no React plugin here, and "neither surface has JSX" is NOT a
|
||||
// sufficient reason for that — `core/react/` is a directory of React hooks,
|
||||
// which the hook rules judge whether or not any JSX is present. So the whole
|
||||
// `eslint-plugin-react-hooks` set, `set-state-in-effect` included, has never
|
||||
// looked at them: it is a devDependency of `clients/web` only, and Node
|
||||
// resolution walks up rather than down, so the root cannot even load it.
|
||||
// That is why the prop-into-effect re-sync #1955 fixed sat here unreported
|
||||
// while the identical shape is an error over in `clients/web/src`. Closing the
|
||||
// gap means adding the plugin at the root and taking what it then reports —
|
||||
// measured during #1955 as 15 findings, every one of them in a hook that issue
|
||||
// did not convert (the two settings-draft hooks and the three async fetch
|
||||
// hooks) and none in the 16 it did. Tracked on its own rather than folded in.
|
||||
// `core/react/` gets the React hook rules, and "neither surface has JSX" was
|
||||
// never a reason to withhold them — the hook rules judge hooks, not JSX, and
|
||||
// that directory is nothing but hooks. Until #2192 the whole
|
||||
// `eslint-plugin-react-hooks` set, `set-state-in-effect` included, had never
|
||||
// looked at them: every copy of the plugin was *client-local* (a devDependency
|
||||
// of `clients/web` and of `clients/tui`), and Node resolution walks up rather
|
||||
// than down, so the root could reach neither. That is how the prop-into-effect
|
||||
// re-sync #1955 fixed sat here unreported while the identical shape was an
|
||||
// error over in `clients/web/src`. The plugin is now a root devDependency too,
|
||||
// and the block below applies it.
|
||||
//
|
||||
// Type-aware linting for the two blocks below. `no-floating-promises` needs
|
||||
// type information, which `tseslint.configs.recommended` (the non-type-aware
|
||||
// set) does not provide — so each block adds a parser project (#1959).
|
||||
// Scoped to `core/react/**` rather than all of `core/**`: everything else
|
||||
// there is plain isomorphic TypeScript, and the rules key off the `use`-prefix
|
||||
// naming convention, so a non-React `useFoo` helper elsewhere in `core/` would
|
||||
// be judged as a hook it is not.
|
||||
//
|
||||
// Type-aware linting for the `core/` and shared surfaces below.
|
||||
// `no-floating-promises` needs type information, which
|
||||
// `tseslint.configs.recommended` (the non-type-aware set) does not provide —
|
||||
// so a block of its own adds a parser project (#1959).
|
||||
//
|
||||
// `tsconfig.lint.json` exists for this and nothing else: the root has no
|
||||
// tsconfig of its own (`core/` is typechecked through `clients/web`'s
|
||||
|
|
@ -81,6 +86,17 @@ export default defineConfig([
|
|||
},
|
||||
rules: sharedRules,
|
||||
},
|
||||
{
|
||||
// #2192. `exhaustive-deps` is promoted from the recommended set's `warn`
|
||||
// for the same reason `clients/web` promotes it (#2085): `--max-warnings 0`
|
||||
// already fails the gate on it, and matching the severity keeps an editor
|
||||
// squiggle honest rather than depending on a CLI flag.
|
||||
files: ["core/react/**/*.{ts,tsx}"],
|
||||
extends: [reactHooks.configs.flat.recommended],
|
||||
rules: {
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"test-servers/src/**/*.{ts,tsx,mts,cts}",
|
||||
|
|
|
|||
554
package-lock.json
generated
554
package-lock.json
generated
|
|
@ -40,6 +40,7 @@
|
|||
"@eslint/js": "^10.0.1",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"express": "^5.2.1",
|
||||
"globals": "^17.7.0",
|
||||
"prettier": "3.8.4",
|
||||
|
|
@ -63,6 +64,266 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.2.3",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
|
||||
"integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@babel/types": "^7.29.8",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
||||
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.8"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
|
||||
"integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.8",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.8",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "2.0.0-alpha.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
|
||||
|
|
@ -300,6 +561,56 @@
|
|||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/client": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
|
||||
|
|
@ -1417,6 +1728,19 @@
|
|||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.11.20",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
|
||||
"integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
|
|
@ -1467,6 +1791,40 @@
|
|||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.8",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
"electron-to-chromium": "^1.5.402",
|
||||
"node-releases": "^2.0.53",
|
||||
"update-browserslist-db": "^1.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
|
|
@ -1520,6 +1878,27 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001810",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
|
||||
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
|
|
@ -1633,6 +2012,13 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-to-spaces": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
|
||||
|
|
@ -1793,6 +2179,13 @@
|
|||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.416",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz",
|
||||
"integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
|
|
@ -1861,6 +2254,16 @@
|
|||
"tests/types"
|
||||
]
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
|
|
@ -1935,6 +2338,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
|
||||
"integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
|
|
@ -2326,6 +2749,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
|
|
@ -2443,6 +2876,23 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hermes-estree": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||
"integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hermes-parser": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
|
||||
"integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.1",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz",
|
||||
|
|
@ -2715,6 +3165,26 @@
|
|||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
|
|
@ -2742,6 +3212,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
|
@ -3043,6 +3526,16 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -3163,6 +3656,16 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.54",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
|
||||
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -4186,6 +4689,37 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
|
||||
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
|
@ -4404,6 +4938,13 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
|
|
@ -4456,6 +4997,19 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25.28 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-validation-error": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
|
||||
"integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
"@eslint/js": "^10.0.1",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"express": "^5.2.1",
|
||||
"globals": "^17.7.0",
|
||||
"prettier": "3.8.4",
|
||||
|
|
|
|||
Loading…
Reference in a new issue