diff --git a/examples/todoapp/src/App.tsx b/examples/todoapp/src/App.tsx
index 1ad1f60b..e332036f 100644
--- a/examples/todoapp/src/App.tsx
+++ b/examples/todoapp/src/App.tsx
@@ -21,7 +21,11 @@ function App() {
input: [
{ field: "id", config: { hidden: true } }
{ field: "hasDone", config: { hidden: true } }
- { field: "name", config: { gridCol: 9 } }
+ {
+ field: "name"
+ config: { gridCol: 9 }
+ constraint: { maxLength: 50 }
+ }
{ field: "priority", config: { gridCol: 3 } }
]
) {
diff --git a/packages/chakra-ui/src/components/default/form.tsx b/packages/chakra-ui/src/components/default/form.tsx
index 3b03935a..48eecc1a 100644
--- a/packages/chakra-ui/src/components/default/form.tsx
+++ b/packages/chakra-ui/src/components/default/form.tsx
@@ -3,7 +3,7 @@ import {
FormComponentProps,
FieldType,
} from "@fabrix-framework/fabrix";
-import { Switch, Input, Stack, Button, Box, Text } from "@chakra-ui/react";
+import { Text, Switch, Input, Stack, Button, Box } from "@chakra-ui/react";
import { Select } from "chakra-react-select";
import { useController } from "@fabrix-framework/fabrix/rhf";
import { LabelledHeading } from "./shared";
@@ -121,6 +121,7 @@ const SelectFormField = (
onBlur={field.onBlur}
onChange={(e) => e && field.onChange(e.value)}
/>
+
);
};
@@ -136,6 +137,7 @@ const TextFormField = (props: FormFieldComponentProps) => {
+
);
};
@@ -173,6 +175,7 @@ const BooleanFormField = (props: FormFieldComponentProps) => {
+
);
};
diff --git a/packages/fabrix/tests/mocks/data.ts b/packages/fabrix/__tests__/mocks/data.ts
similarity index 73%
rename from packages/fabrix/tests/mocks/data.ts
rename to packages/fabrix/__tests__/mocks/data.ts
index df853cf9..41a4c433 100644
--- a/packages/fabrix/tests/mocks/data.ts
+++ b/packages/fabrix/__tests__/mocks/data.ts
@@ -4,11 +4,11 @@ export const users = [
{
id: faker.string.uuid(),
name: "first user",
- code: "u001",
+ email: faker.internet.email(),
},
{
id: faker.string.uuid(),
name: "second user",
- code: "u002",
+ email: faker.internet.email(),
},
];
diff --git a/packages/fabrix/tests/mocks/handlers.ts b/packages/fabrix/__tests__/mocks/handlers.ts
similarity index 94%
rename from packages/fabrix/tests/mocks/handlers.ts
rename to packages/fabrix/__tests__/mocks/handlers.ts
index 9fa5836f..963ba65e 100644
--- a/packages/fabrix/tests/mocks/handlers.ts
+++ b/packages/fabrix/__tests__/mocks/handlers.ts
@@ -7,7 +7,7 @@ const mockSchema = buildSchema(`
type User {
id: ID!
name: String!
- code: String!
+ email: String!
}
type UsersResult {
@@ -26,9 +26,9 @@ enum UserCategory {
input CreateUserInput {
id: ID
name: String!
- code: String!
+ email: String
age: Int!
- category: UserCategory!
+ category: UserCategory
}
type Mutation {
diff --git a/packages/fabrix/tests/mocks/server.ts b/packages/fabrix/__tests__/mocks/server.ts
similarity index 100%
rename from packages/fabrix/tests/mocks/server.ts
rename to packages/fabrix/__tests__/mocks/server.ts
diff --git a/packages/fabrix/__tests__/mutation.test.tsx b/packages/fabrix/__tests__/mutation.test.tsx
new file mode 100644
index 00000000..89b39ff8
--- /dev/null
+++ b/packages/fabrix/__tests__/mutation.test.tsx
@@ -0,0 +1,51 @@
+import { describe, expect, it } from "vitest";
+import { screen, within } from "@testing-library/react";
+import { FabrixComponent } from "@renderer";
+import { testWithUnmount } from "./supports/render";
+
+describe("mutation", () => {
+ it("should render the form", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await screen.findByRole("form");
+ expect(form).toBeInTheDocument();
+
+ const inputs = await within(form).findAllByRole("textbox");
+ expect(inputs.length).toBe(5);
+ },
+ );
+ });
+
+ it("should render the form with customized labels", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await screen.findByRole("form");
+ expect(form).toBeInTheDocument();
+
+ expect(within(form).queryByLabelText("id")).not.toBeInTheDocument();
+ expect(within(form).getByLabelText("UserName")).toBeInTheDocument();
+ },
+ );
+ });
+});
diff --git a/packages/fabrix/__tests__/mutationWithValidation.test.tsx b/packages/fabrix/__tests__/mutationWithValidation.test.tsx
new file mode 100644
index 00000000..0e586b00
--- /dev/null
+++ b/packages/fabrix/__tests__/mutationWithValidation.test.tsx
@@ -0,0 +1,268 @@
+import { describe, expect, it } from "vitest";
+import { FabrixComponent } from "@renderer";
+import { faker } from "@faker-js/faker";
+import { testWithUnmount } from "./supports/render";
+import { findForm } from "./supports/utils";
+
+describe("String", () => {
+ it("minLength/maxLength", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", faker.string.alpha(4));
+ await form.submit();
+ expect(form.getAlert("name")).toHaveTextContent(
+ "must NOT have fewer than 5 characters",
+ );
+
+ await form.set("name", faker.string.alpha(5));
+ await form.submit();
+ expect(form.getAlert("name")).not.toBeInTheDocument();
+
+ await form.set("name", faker.string.alpha(10));
+ await form.submit();
+ expect(form.getAlert("name")).not.toBeInTheDocument();
+
+ await form.set("name", faker.string.alpha(11));
+ await form.submit();
+ expect(form.getAlert("name")).toHaveTextContent(
+ "must NOT have more than 10 characters",
+ );
+ },
+ );
+ });
+
+ it("pattern", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", "John Doe");
+ await form.submit();
+ expect(form.getAlert("name")).toHaveTextContent(
+ 'must match pattern "^[a-z]+$"',
+ );
+
+ await form.set("name", "johndoe");
+ await form.submit();
+ expect(form.getAlert("name")).not.toBeInTheDocument();
+ },
+ );
+ });
+
+ it("format (email)", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("email", "john.doe");
+ await form.submit();
+ expect(form.getAlert("email")).toHaveTextContent(
+ 'must match format "email"',
+ );
+
+ await form.set("email", faker.internet.email());
+ await form.submit();
+ expect(form.getAlert("email")).not.toBeInTheDocument();
+ },
+ );
+ });
+
+ it("oneOf", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", "EmployeeX");
+ await form.submit();
+ expect(form.getAlert("name")).toHaveTextContent(
+ "must be equal to one of the allowed value",
+ );
+
+ await form.set("name", "EmployeeA");
+ await form.submit();
+ expect(form.getAlert("name")).not.toBeInTheDocument();
+ },
+ );
+ });
+});
+
+describe("Int/Float", () => {
+ it("min/max", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", "John Doe");
+ await form.set("category", "ADMIN");
+
+ await form.set("age", "19");
+ await form.submit();
+ expect(form.getAlert("age")).toHaveTextContent("must be >= 20");
+
+ await form.set("age", "20");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+
+ await form.set("age", "30");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+
+ await form.set("age", "31");
+ await form.submit();
+ expect(form.getAlert("age")).toHaveTextContent("must be <= 30");
+ },
+ );
+ });
+
+ it("exclusive min/max", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", "John Doe");
+ await form.set("category", "ADMIN");
+
+ await form.set("age", "20");
+ await form.submit();
+ expect(form.getAlert("age")).toHaveTextContent("must be > 20");
+
+ await form.set("age", "21");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+
+ await form.set("age", "29");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+
+ await form.set("age", "30");
+ await form.submit();
+ expect(form.getAlert("age")).toHaveTextContent("must be < 30");
+ },
+ );
+ });
+
+ it("multipleOf", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", "John Doe");
+
+ await form.set("age", "19");
+ await form.submit();
+ expect(form.getAlert("age")).toHaveTextContent("must be multiple of 5");
+
+ await form.set("age", "20");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+ },
+ );
+ });
+
+ it("oneOf", async () => {
+ await testWithUnmount(
+ ,
+ async () => {
+ const form = await findForm();
+ await form.set("name", "John Doe");
+ await form.set("category", "ADMIN");
+
+ await form.set("age", "19");
+ await form.submit();
+ expect(form.getAlert("age")).toHaveTextContent(
+ "must be equal to one of the allowed value",
+ );
+
+ await form.set("age", "20");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+
+ await form.set("age", "30");
+ await form.submit();
+ expect(form.getAlert("age")).not.toBeInTheDocument();
+ },
+ );
+ });
+});
diff --git a/packages/fabrix/src/renders.test.tsx b/packages/fabrix/__tests__/query.test.tsx
similarity index 68%
rename from packages/fabrix/src/renders.test.tsx
rename to packages/fabrix/__tests__/query.test.tsx
index 930e2caf..1b654e04 100644
--- a/packages/fabrix/src/renders.test.tsx
+++ b/packages/fabrix/__tests__/query.test.tsx
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
import { screen, within } from "@testing-library/react";
import { FabrixComponent } from "@renderer";
import { ComponentRegistry } from "@registry";
-import { users } from "../tests/mocks/data";
-import { testWithUnmount } from "../tests/render";
+import { users } from "./mocks/data";
+import { testWithUnmount } from "./supports/render";
describe("query", () => {
it("should render the table with collection", async () => {
@@ -15,7 +15,7 @@ describe("query", () => {
collection {
id
name
- code
+ email
}
}
}
@@ -42,7 +42,7 @@ describe("query", () => {
collection {
id
name
- code
+ email
}
}
}
@@ -103,7 +103,7 @@ describe("query", () => {
collection {
id
name
- code
+ email
}
}
}
@@ -123,52 +123,3 @@ describe("query", () => {
);
});
});
-
-describe("mutation", () => {
- it("should render the form", async () => {
- await testWithUnmount(
- ,
- async () => {
- const form = await screen.findByRole("form");
- expect(form).toBeInTheDocument();
-
- const inputs = await within(form).findAllByRole("textbox");
- expect(inputs.length).toBe(5);
- },
- );
- });
-
- it("should render the form with customized labels", async () => {
- await testWithUnmount(
- ,
- async () => {
- const form = await screen.findByRole("form");
- expect(form).toBeInTheDocument();
-
- expect(within(form).queryByLabelText("id")).not.toBeInTheDocument();
- expect(within(form).getByLabelText("name")).toHaveTextContent(
- "UserName",
- );
- },
- );
- });
-});
diff --git a/packages/fabrix/tests/setup.ts b/packages/fabrix/__tests__/setup.ts
similarity index 100%
rename from packages/fabrix/tests/setup.ts
rename to packages/fabrix/__tests__/setup.ts
diff --git a/packages/fabrix/tests/components.tsx b/packages/fabrix/__tests__/supports/components.tsx
similarity index 61%
rename from packages/fabrix/tests/components.tsx
rename to packages/fabrix/__tests__/supports/components.tsx
index 0707c98a..fcbb71d4 100644
--- a/packages/fabrix/tests/components.tsx
+++ b/packages/fabrix/__tests__/supports/components.tsx
@@ -6,6 +6,7 @@ import {
TableComponentProps,
} from "@registry";
import { ReactNode } from "react";
+import { useController } from "react-hook-form";
const fieldView = (props: FieldComponentProps) => {
const { value } = props;
@@ -48,13 +49,32 @@ const formView = (props: FormComponentProps) => {
};
const formFieldView = (props: FormFieldComponentProps) => {
+ const { field, formState } = useController({
+ name: props.name,
+ defaultValue: "",
+ });
+ const error = formState.errors[props.name];
+ const isNumber =
+ props.type?.type === "Scalar" &&
+ (props.type.name === "Int" || props.type.name === "Float");
+
return (
-
-
-
-
+
);
};
diff --git a/packages/fabrix/tests/render.tsx b/packages/fabrix/__tests__/supports/render.tsx
similarity index 100%
rename from packages/fabrix/tests/render.tsx
rename to packages/fabrix/__tests__/supports/render.tsx
diff --git a/packages/fabrix/__tests__/supports/utils.ts b/packages/fabrix/__tests__/supports/utils.ts
new file mode 100644
index 00000000..e349980f
--- /dev/null
+++ b/packages/fabrix/__tests__/supports/utils.ts
@@ -0,0 +1,25 @@
+import { screen } from "@testing-library/react";
+import { within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+export const findForm = async () => {
+ const form = await screen.findByRole("form");
+ const user = userEvent.setup();
+
+ return {
+ element: within(form),
+ set: async (label: string, value: string) => {
+ const input = within(form).getByLabelText(label);
+ await user.clear(input);
+ await user.type(input, value);
+ },
+ submit: async () => {
+ const submit = within(form).getByRole("button");
+ await user.click(submit);
+ },
+ getAlert: (label: string) => {
+ const field = within(form).getByLabelText(`field:${label}`);
+ return within(field).queryByRole("alert");
+ },
+ };
+};
diff --git a/packages/fabrix/package.json b/packages/fabrix/package.json
index 59fb23c0..65f04b32 100644
--- a/packages/fabrix/package.json
+++ b/packages/fabrix/package.json
@@ -27,6 +27,10 @@
"author": "",
"license": "MIT",
"dependencies": {
+ "@hookform/resolvers": "^3.9.0",
+ "ajv": "^8.17.1",
+ "ajv-errors": "^3.0.0",
+ "ajv-formats": "^3.0.1",
"deepmerge-ts": "^7.1.0",
"graphql-tag": "^2.12.6",
"react-hook-form": "^7.53.0",
diff --git a/packages/fabrix/src/directive.ts b/packages/fabrix/src/directive.ts
index de53bb24..9ecbae98 100644
--- a/packages/fabrix/src/directive.ts
+++ b/packages/fabrix/src/directive.ts
@@ -14,35 +14,6 @@ export const parseDirectiveArguments = (
return parsedValue.data;
};
-type FieldConfig = {
- config: C;
- field?: string | null;
-};
-
-/**
- * A helper function to build a record from object array that has `field` key
- */
-export const buildRecordByFieldName = <
- C extends Record,
- V extends FieldConfig = FieldConfig,
- T extends Array = Array,
->(
- input: T,
-) => {
- return input.reduce>(
- (acc, value) =>
- value.field
- ? {
- ...acc,
- [value.field]: {
- config: value.config,
- },
- }
- : acc,
- {},
- );
-};
-
export const findDirective = (
directives: ReadonlyArray | undefined,
) => {
diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts
index 23fcc00a..7b3498f1 100644
--- a/packages/fabrix/src/directive/schema.ts
+++ b/packages/fabrix/src/directive/schema.ts
@@ -35,6 +35,26 @@ export const formFieldSchema = baseFieldSchema.merge(
}),
);
+export const formFieldConstraintSchema = z
+ .object({
+ // String constraints
+ minLength: z.number().nullish(),
+ maxLength: z.number().nullish(),
+ pattern: z.string().nullish(),
+ format: z.string().nullish(),
+
+ // Number constraints
+ min: z.number().nullish(),
+ max: z.number().nullish(),
+ exclusiveMin: z.number().nullish(),
+ exclusiveMax: z.number().nullish(),
+ multipleOf: z.number().nullish(),
+
+ // String/Number constraints
+ oneOf: z.array(z.string().or(z.number())).nullish(),
+ })
+ .nullish();
+
export const viewFieldSchema = baseFieldSchema.merge(
z.object({
gridCol: z
@@ -89,6 +109,7 @@ export const directiveSchemaMap = {
hidden: defaultValues.hidden,
}),
),
+ constraint: formFieldConstraintSchema,
}),
),
}),
diff --git a/packages/fabrix/src/inferer.ts b/packages/fabrix/src/inferer.ts
deleted file mode 100644
index 37bc6261..00000000
--- a/packages/fabrix/src/inferer.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { FabrixContextType } from "@context";
-import { formFieldSchema, viewFieldSchema } from "@directive/schema";
-import { resolveFieldType } from "@renderers/shared";
-import { Fields, FieldVariables, Path } from "@visitor";
-import { deepmerge } from "deepmerge-ts";
-import { GraphQLInputObjectType, GraphQLNonNull } from "graphql";
-
-export type FieldWithDirective<
- C extends Record = Record,
- M extends Record = Record,
-> = {
- field: Path;
- config: C;
- meta: M;
-};
-
-type DirectiveInput<
- C extends Record = Record,
-> = Array<{
- field: Path;
- config: C;
-}>;
-
-/*
- * Merge the default field configs with the input field configs
- */
-export const mergeFieldConfigs = <
- C extends Record,
- M extends Record,
->(
- fieldConfigs: Array>,
- directiveInput: DirectiveInput,
-) => {
- const allFieldKeys = new Set([
- ...fieldConfigs.map((f) => f.field.asKey()),
- ...directiveInput.map((f) => f.field.asKey()),
- ]);
-
- return Array.from(allFieldKeys).flatMap((key) => {
- const directiveValue = directiveInput.find((f) => f.field.asKey() === key);
- const fieldValue = fieldConfigs.find((f) => f.field.asKey() === key);
- const field = directiveValue?.field || fieldValue?.field;
- if (!field) {
- return [];
- }
-
- const mergeConfig = () => {
- if (fieldValue && directiveValue) {
- return {
- config: deepmerge<[C, C]>(fieldValue.config, directiveValue.config),
- meta: fieldValue.meta,
- };
- } else if (fieldValue) {
- return { config: fieldValue.config, meta: fieldValue.meta };
- } else if (directiveValue) {
- return { config: directiveValue.config, meta: {} };
- } else {
- return null;
- }
- };
-
- const mergedValue = mergeConfig();
- if (!mergedValue) {
- return [];
- }
-
- return {
- field,
- config: mergedValue.config,
- meta: mergedValue.meta,
- };
- });
-};
-
-/**
- * Infer the field configuration from the fields
- */
-export const buildDefaultViewFieldConfigs = (fields: Fields) =>
- fields.unwrap().flatMap((field) => {
- const config = viewFieldSchema.parse({
- index: 0,
- label: field.getName(),
- });
-
- // This field configs are used to be merged into the field configs on directives
- // So we need to skip the root path here.
- const path = field.value.path.rootOffset(1);
- if (!path) {
- return [];
- }
-
- return {
- field: path,
- config,
- meta: {},
- };
- });
-
-/**
- * Infer the field configuration from the input object type for the form
- */
-export const buildDefaultFormFieldConfigs = (
- context: FabrixContextType,
- fieldVariables: FieldVariables,
-) => {
- if (!("input" in fieldVariables)) {
- return [];
- }
-
- if (context.schemaLoader.status === "loading") {
- return [];
- }
-
- const inputType = context.schemaLoader.schemaSet.serverSchema.getType(
- fieldVariables.input.type,
- );
- if (!inputType) {
- return [];
- }
-
- // Only support object type for "input" argument
- if (!(inputType instanceof GraphQLInputObjectType)) {
- return [];
- }
-
- const fields = inputType.getFields();
- return Object.keys(fields).map((key, index) => {
- const field = fields[key];
- const path = new Path(key.split("."));
-
- return {
- field: path,
- meta: {
- fieldType: resolveFieldType(
- field.type instanceof GraphQLNonNull ? field.type.ofType : field.type,
- ),
- isRequired: field.type instanceof GraphQLNonNull,
- },
- config: formFieldSchema.parse({
- index,
- label: field.name,
- }),
- };
- });
-};
diff --git a/packages/fabrix/src/readers/field.ts b/packages/fabrix/src/readers/field.ts
new file mode 100644
index 00000000..794526f2
--- /dev/null
+++ b/packages/fabrix/src/readers/field.ts
@@ -0,0 +1,46 @@
+import { viewFieldSchema } from "@directive/schema";
+import { FieldConfigWithMeta, FieldConfig } from "@readers/shared";
+import { Fields } from "@visitor";
+import { deepmerge } from "deepmerge-ts";
+
+/**
+ * Infer the field configuration from the fields
+ */
+export const buildDefaultViewFieldConfigs = (fields: Fields) =>
+ fields.unwrap().flatMap((field) => {
+ const config = viewFieldSchema.parse({
+ index: 0,
+ label: field.getName(),
+ });
+
+ // This field configs are used to be merged into the field configs on directives
+ // So we need to skip the root path here.
+ const path = field.value.path.rootOffset(1);
+ if (!path) {
+ return [];
+ }
+
+ return {
+ field: path,
+ config,
+ meta: null,
+ };
+ });
+
+export const viewFieldMerger = >(
+ fieldValue: FieldConfigWithMeta | undefined,
+ directiveValue: FieldConfig | undefined,
+) => {
+ if (fieldValue && directiveValue) {
+ return {
+ config: deepmerge(fieldValue.config, directiveValue.config),
+ meta: fieldValue.meta,
+ };
+ } else if (fieldValue) {
+ return { config: fieldValue.config, meta: fieldValue.meta };
+ } else if (directiveValue) {
+ return { config: directiveValue.config, meta: null };
+ } else {
+ return null;
+ }
+};
diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts
new file mode 100644
index 00000000..763ddbc8
--- /dev/null
+++ b/packages/fabrix/src/readers/form.ts
@@ -0,0 +1,98 @@
+import { FabrixContextType } from "@context";
+import {
+ formFieldConstraintSchema,
+ FormFieldSchema,
+ formFieldSchema,
+} from "@directive/schema";
+import { resolveFieldType } from "@renderers/shared";
+import { FieldVariables, Path } from "@visitor";
+import { deepmerge } from "deepmerge-ts";
+import {
+ GraphQLInputObjectType,
+ GraphQLInputType,
+ GraphQLNonNull,
+} from "graphql";
+import { z } from "zod";
+import { FieldConfigWithMeta, FieldConfig } from "./shared";
+
+const buildFieldMeta = (type: GraphQLInputType) => ({
+ fieldType: resolveFieldType(
+ type instanceof GraphQLNonNull ? type.ofType : type,
+ ),
+ isRequired: type instanceof GraphQLNonNull,
+});
+
+export type FieldMeta = ReturnType | null;
+
+/**
+ * Infer the field configuration from the input object type for the form
+ */
+export const buildDefaultFormFieldConfigs = (
+ context: FabrixContextType,
+ fieldVariables: FieldVariables,
+) => {
+ if (!("input" in fieldVariables)) {
+ return [];
+ }
+
+ if (context.schemaLoader.status === "loading") {
+ return [];
+ }
+
+ const inputType = context.schemaLoader.schemaSet.serverSchema.getType(
+ fieldVariables.input.type,
+ );
+ if (!inputType) {
+ return [];
+ }
+
+ // Only support object type for "input" argument
+ if (!(inputType instanceof GraphQLInputObjectType)) {
+ return [];
+ }
+
+ const fields = inputType.getFields();
+ return Object.keys(fields).map((key, index) => {
+ const field = fields[key];
+ const path = new Path(key.split("."));
+
+ return {
+ field: path,
+ meta: buildFieldMeta(field.type),
+ config: formFieldSchema.parse({
+ index,
+ label: field.name,
+ }),
+ };
+ });
+};
+
+export type FormFieldExtra = {
+ constraint?: z.infer;
+};
+
+export const formFieldMerger = (
+ fieldValue: FieldConfigWithMeta | undefined,
+ directiveValue: FieldConfig | undefined,
+) => {
+ if (fieldValue && directiveValue) {
+ return {
+ config: deepmerge(fieldValue.config, directiveValue.config),
+ meta: fieldValue.meta,
+ constraint: directiveValue.constraint,
+ };
+ } else if (fieldValue) {
+ return {
+ config: fieldValue.config,
+ meta: fieldValue.meta,
+ };
+ } else if (directiveValue) {
+ return {
+ config: directiveValue.config,
+ meta: null,
+ constraint: directiveValue.constraint,
+ };
+ } else {
+ return null;
+ }
+};
diff --git a/packages/fabrix/src/readers/shared.ts b/packages/fabrix/src/readers/shared.ts
new file mode 100644
index 00000000..6a8a17e1
--- /dev/null
+++ b/packages/fabrix/src/readers/shared.ts
@@ -0,0 +1,67 @@
+import { FieldType } from "@renderers/shared";
+import { Path } from "@visitor";
+
+type FieldMeta = {
+ fieldType: FieldType;
+ isRequired: boolean;
+} | null;
+
+export type FieldConfig<
+ C extends Record,
+ E extends Record = Record,
+> = {
+ field: Path;
+ config: C;
+} & E;
+
+export type FieldConfigWithMeta> =
+ FieldConfig<
+ C,
+ {
+ meta: FieldMeta;
+ }
+ >;
+
+type Merger<
+ C extends Record,
+ E extends Record,
+> = (
+ fieldValue: FieldConfigWithMeta | undefined,
+ directiveValue: FieldConfig | undefined,
+) => Omit & E, "field"> | null;
+
+/*
+ * Merge the default field configs with the input field configs
+ */
+export const mergeFieldConfigs = <
+ C extends Record,
+ E extends Record,
+>(
+ fieldConfigs: Array>,
+ directiveInput: Array>,
+ merger: Merger,
+) => {
+ const allFieldKeys = new Set([
+ ...fieldConfigs.map((f) => f.field.asKey()),
+ ...directiveInput.map((f) => f.field.asKey()),
+ ]);
+
+ return Array.from(allFieldKeys).flatMap((key) => {
+ const directiveValue = directiveInput.find((f) => f.field.asKey() === key);
+ const fieldValue = fieldConfigs.find((f) => f.field.asKey() === key);
+ const field = directiveValue?.field || fieldValue?.field;
+ if (!field) {
+ return [];
+ }
+
+ const mergedValue = merger(fieldValue, directiveValue);
+ if (!mergedValue) {
+ return [];
+ }
+
+ return {
+ field,
+ ...mergedValue,
+ };
+ });
+};
diff --git a/packages/fabrix/src/renderer.tsx b/packages/fabrix/src/renderer.tsx
index 837e8926..2a64e064 100644
--- a/packages/fabrix/src/renderer.tsx
+++ b/packages/fabrix/src/renderer.tsx
@@ -10,11 +10,9 @@ import {
CommonFabrixComponentRendererProps,
} from "@renderers/shared";
import { directiveSchemaMap } from "@directive/schema";
-import {
- buildDefaultViewFieldConfigs,
- buildDefaultFormFieldConfigs,
- mergeFieldConfigs,
-} from "@inferer";
+import { mergeFieldConfigs } from "@readers/shared";
+import { buildDefaultViewFieldConfigs, viewFieldMerger } from "@readers/field";
+import { buildDefaultFormFieldConfigs, formFieldMerger } from "@readers/form";
import { buildRootDocument, Field, Fields, FieldVariables } from "@/visitor";
import { FabrixComponentData } from "@/fetcher";
@@ -73,6 +71,7 @@ const getFieldConfig = (
fields: mergeFieldConfigs(
buildDefaultViewFieldConfigs(childFields),
directive.input,
+ viewFieldMerger,
),
},
};
@@ -100,6 +99,7 @@ const getFieldConfig = (
fields: mergeFieldConfigs(
buildDefaultFormFieldConfigs(context, fieldVariables),
directive.input,
+ formFieldMerger,
),
},
};
diff --git a/packages/fabrix/src/renderers/fields.tsx b/packages/fabrix/src/renderers/fields.tsx
index 8f19f103..c9924bff 100644
--- a/packages/fabrix/src/renderers/fields.tsx
+++ b/packages/fabrix/src/renderers/fields.tsx
@@ -1,6 +1,6 @@
import { createElement, useCallback, useMemo } from "react";
import { ViewFieldSchema } from "@directive/schema";
-import { FieldWithDirective } from "@inferer";
+import { FieldConfigWithMeta } from "@readers/shared";
import { FabrixContextType } from "@context";
import { useDataFetch, Value } from "../fetcher";
import { RendererCommonProps } from "../renderer";
@@ -13,7 +13,7 @@ import {
resolveFieldTypesFromTypename,
} from "./shared";
-type ViewField = FieldWithDirective;
+type ViewField = FieldConfigWithMeta;
type Fields = Array;
export const ViewRenderer = (
diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx
index 4c40a749..7da6e68f 100644
--- a/packages/fabrix/src/renderers/form.tsx
+++ b/packages/fabrix/src/renderers/form.tsx
@@ -2,32 +2,20 @@ import { createElement, useCallback } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useMutation } from "urql";
import { FormFieldSchema } from "@directive/schema";
-import { FieldWithDirective } from "@inferer";
+import { FieldConfigWithMeta } from "@readers/shared";
+import { FormFieldExtra } from "@readers/form";
import { FabrixContextType } from "../context";
import {
buildClassName,
CommonFabrixComponentRendererProps,
- FieldType,
+ defaultFieldType,
getFieldConfigByKey,
Loader,
} from "./shared";
+import { buildAjvSchema } from "./form/validation";
+import { ajvResolver } from "./form/ajvResolver";
-const getClearedValue = (values: Record) =>
- Object.keys(values).reduce((acc, key) => {
- return {
- ...acc,
- [key]: null,
- };
- }, {});
-
-export type FormFieldMeta =
- | {
- fieldType: FieldType;
- isRequired: boolean;
- }
- | Record;
-
-export type FormField = FieldWithDirective;
+export type FormField = FieldConfigWithMeta & FormFieldExtra;
export const FormRenderer = (
props: CommonFabrixComponentRendererProps<{
@@ -35,21 +23,18 @@ export const FormRenderer = (
}>,
) => {
const { context, fieldConfigs, query, componentFieldsRenderer } = props;
- const formContext = useForm();
+ const formContext = useForm({
+ resolver: ajvResolver(buildAjvSchema(fieldConfigs.fields)),
+ });
const [mutationResult, runMutation] = useMutation(query.documentResolver());
+ const runSubmit = formContext.handleSubmit(async (input) => {
+ // TODO: sending values should be specifiable by the user through something like `path`
+ await runMutation({
+ input,
+ });
- const runSubmit = useCallback(() => {
- runMutation({
- // TODO: here should be specifiable by the user through `path`
- input: formContext.getValues(),
- })
- .then(() => {
- formContext.reset(getClearedValue(formContext.getValues()));
- })
- .catch((error) => {
- throw error;
- });
- }, [formContext, runMutation]);
+ formContext.reset();
+ });
const renderFields = useCallback(() => {
if (componentFieldsRenderer) {
@@ -141,9 +126,9 @@ const renderField = (props: {
return createElement(component, {
key: indexKey,
value: fieldConfig.defaultValue,
- type: field.meta.fieldType,
+ type: field.meta?.fieldType ?? defaultFieldType,
name: field.field.asKey(),
- isRequired: field.meta.isRequired,
+ isRequired: field.meta?.isRequired ?? false,
attributes: {
className,
label: fieldConfig.label,
diff --git a/packages/fabrix/src/renderers/form/ajvResolver.ts b/packages/fabrix/src/renderers/form/ajvResolver.ts
new file mode 100644
index 00000000..ced58a38
--- /dev/null
+++ b/packages/fabrix/src/renderers/form/ajvResolver.ts
@@ -0,0 +1,112 @@
+// Implementation copy-pasted from the original (https://github.com/react-hook-form/resolvers/blob/master/ajv/src/ajv.ts)
+// we would like to extends Ajv to use ajv-formats for our requirements, but built-in RHF resolvers are not extendable.
+
+import { toNestErrors, validateFieldsNatively } from "@hookform/resolvers";
+import Ajv, { DefinedError, JSONSchemaType, Options } from "ajv";
+import ajvErrors from "ajv-errors";
+import ajvFormats from "ajv-formats";
+import {
+ FieldError,
+ FieldValues,
+ ResolverOptions,
+ ResolverResult,
+ appendErrors,
+} from "react-hook-form";
+
+type Resolver = (
+ schema: JSONSchemaType,
+ schemaOptions?: Options,
+ factoryOptions?: { mode?: "async" | "sync" },
+) => (
+ values: TFieldValues,
+ context: TContext | undefined,
+ options: ResolverOptions,
+) => Promise>;
+
+const parseErrorSchema = (
+ ajvErrors: DefinedError[],
+ validateAllFieldCriteria: boolean,
+) => {
+ // Ajv will return empty instancePath when require error
+ ajvErrors.forEach((error) => {
+ if (error.keyword === "required") {
+ error.instancePath += "/" + error.params.missingProperty;
+ }
+ });
+
+ return ajvErrors.reduce>((previous, error) => {
+ // `/deepObject/data` -> `deepObject.data`
+ const path = error.instancePath.substring(1).replace(/\//g, ".");
+
+ if (!previous[path]) {
+ previous[path] = {
+ message: error.message,
+ type: error.keyword,
+ };
+ }
+
+ if (validateAllFieldCriteria) {
+ const types = previous[path].types;
+ const messages = types && types[error.keyword];
+
+ previous[path] = appendErrors(
+ path,
+ validateAllFieldCriteria,
+ previous,
+ error.keyword,
+ messages
+ ? ([] as string[]).concat(messages as string[], error.message || "")
+ : error.message,
+ ) as FieldError;
+ }
+
+ return previous;
+ }, {});
+};
+
+export const ajvResolver: Resolver =
+ (schema, schemaOptions, resolverOptions = {}) =>
+ async (values, _, options) => {
+ const ajv = new Ajv(
+ Object.assign(
+ {},
+ {
+ allErrors: true,
+ validateSchema: true,
+ },
+ schemaOptions,
+ ),
+ );
+
+ ajvErrors(ajv);
+ ajvFormats(ajv);
+
+ const validate = ajv.compile(
+ Object.assign(
+ { $async: resolverOptions && resolverOptions.mode === "async" },
+ schema,
+ ),
+ );
+
+ const valid = validate(values);
+
+ if (options.shouldUseNativeValidation) {
+ validateFieldsNatively({}, options);
+ }
+
+ return Promise.resolve(
+ valid
+ ? { values, errors: {} }
+ : {
+ values: {},
+ errors: toNestErrors(
+ parseErrorSchema(
+ validate.errors as DefinedError[],
+ !options.shouldUseNativeValidation &&
+ options.criteriaMode === "all",
+ ),
+ options,
+ ),
+ },
+ );
+ };
diff --git a/packages/fabrix/src/renderers/form/validation.ts b/packages/fabrix/src/renderers/form/validation.ts
new file mode 100644
index 00000000..0eba121e
--- /dev/null
+++ b/packages/fabrix/src/renderers/form/validation.ts
@@ -0,0 +1,57 @@
+import { FormField } from "@renderers/form";
+
+const convertToAjvProperty = (field: FormField) => {
+ switch (field.meta?.fieldType?.type) {
+ case "Scalar":
+ switch (field.meta.fieldType.name) {
+ case "Int":
+ case "Float":
+ return {
+ type: "number",
+ maximum: field.constraint?.max,
+ minimum: field.constraint?.min,
+ exclusiveMaximum: field.constraint?.exclusiveMax,
+ exclusiveMinimum: field.constraint?.exclusiveMin,
+ multipleOf: field.constraint?.multipleOf,
+ enum: field.constraint?.oneOf,
+ } as const;
+ case "String":
+ default: {
+ return {
+ type: "string",
+ maxLength: field.constraint?.maxLength,
+ minLength: field.constraint?.minLength,
+ format: field.constraint?.format,
+ pattern: field.constraint?.pattern,
+ enum: field.constraint?.oneOf,
+ } as const;
+ }
+ case "Boolean":
+ return {
+ type: "boolean",
+ } as const;
+ }
+ default:
+ // TODO: handle other types (e.g. object, array)
+ return null;
+ }
+};
+
+export const buildAjvSchema = (fields: Array) => {
+ const visibleFields = fields.filter((field) => !field.config.hidden);
+ const requiredFields = visibleFields.filter(
+ (field) => field.meta?.isRequired,
+ );
+
+ return {
+ type: "object",
+ properties: visibleFields.reduce((acc, field) => {
+ const property = convertToAjvProperty(field);
+ return property === null
+ ? acc
+ : { ...acc, [field.field.asKey()]: property };
+ }, {}),
+ required: requiredFields.map((field) => field.field.asKey()),
+ additionalProperties: true,
+ } as const;
+};
diff --git a/packages/fabrix/src/renderers/shared.tsx b/packages/fabrix/src/renderers/shared.tsx
index 98d8bbaf..fe87a3fe 100644
--- a/packages/fabrix/src/renderers/shared.tsx
+++ b/packages/fabrix/src/renderers/shared.tsx
@@ -10,7 +10,7 @@ import {
} from "graphql";
import { DirectiveAttributes } from "@registry";
import { FabrixContextType } from "@context";
-import { FieldWithDirective } from "@inferer";
+import { FieldConfigWithMeta } from "@readers/shared";
import { FabrixComponentData } from "../fetcher";
type FabrixComponentFieldsRendererExtraProps = Partial & {
@@ -93,11 +93,8 @@ export const assertObjectValue: (
export type FieldTypes = ReturnType;
-export const getFieldConfigByKey = <
- C extends Record = Record,
- M extends Record = Record,
->(
- fields: Array>,
+export const getFieldConfigByKey = >(
+ fields: Array>,
name: string,
) => fields.find((f) => f.field.asKey() == name);
@@ -152,27 +149,34 @@ export const resolveFieldTypesFromTypename = (
}, {});
};
-export type FieldType =
- | {
- type: "Scalar";
- name: string;
- }
- | {
- type: "Enum";
- name: string;
- meta: {
- values: string[];
- };
- }
- | {
- type: "Object";
- name: string;
- }
- | {
- type: "List";
- innerType: NonNullable;
- }
- | null;
+type ScalarType = {
+ type: "Scalar";
+ name: string;
+};
+
+type EnumType = {
+ type: "Enum";
+ name: string;
+ meta: {
+ values: string[];
+ };
+};
+
+type ObjectType = {
+ type: "Object";
+ name: string;
+};
+
+type ListType = {
+ type: "List";
+ innerType: NonNullable;
+};
+
+export type FieldType = ScalarType | EnumType | ObjectType | ListType | null;
+export const defaultFieldType = {
+ type: "Scalar" as const,
+ name: "String",
+};
export const resolveFieldType = (
field: GraphQLOutputType | GraphQLNullableType,
diff --git a/packages/fabrix/vitest.config.ts b/packages/fabrix/vitest.config.ts
index a8739ab3..e2cb7e0b 100644
--- a/packages/fabrix/vitest.config.ts
+++ b/packages/fabrix/vitest.config.ts
@@ -5,8 +5,8 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [viteReact(), tsconfigPaths()],
test: {
- include: ["src/**/*.test.tsx"],
+ include: ["src/**/*.test.tsx", "__tests__/**/*.test.tsx"],
environment: "happy-dom",
- setupFiles: ["./tests/setup.ts"],
+ setupFiles: ["./__tests__/setup.ts"],
},
});
diff --git a/packages/graphql-config/src/schema.ts b/packages/graphql-config/src/schema.ts
index 05eb2284..40014ee4 100644
--- a/packages/graphql-config/src/schema.ts
+++ b/packages/graphql-config/src/schema.ts
@@ -2,6 +2,7 @@ import { DocumentNode, Kind } from "graphql";
import CommonSchema from "./schema/common.graphql";
import ViewDirectiveSchema from "./schema/view.graphql";
import FormDirectiveSchema from "./schema/form.graphql";
+import ConstraintSchema from "./schema/constraint.graphql";
const mergeDocumentNodes = (docs: DocumentNode[]) => ({
kind: Kind.DOCUMENT,
@@ -12,4 +13,5 @@ export const schemaDefinition = mergeDocumentNodes([
CommonSchema,
ViewDirectiveSchema,
FormDirectiveSchema,
+ ConstraintSchema,
]);
diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql
new file mode 100644
index 00000000..8a36731a
--- /dev/null
+++ b/packages/graphql-config/src/schema/constraint.graphql
@@ -0,0 +1,30 @@
+input FabrixFormConstraint {
+ """
+ String
+ """
+ minLength: Int
+ maxLength: Int
+ pattern: String
+ format: String
+ oneOf: [String]
+
+ """
+ Int
+ """
+ min: Int
+ max: Int
+ exclusiveMin: Int
+ exclusiveMax: Int
+ multipleOf: Int
+ oneOf: [Int]
+
+ """
+ Float
+ """
+ min: Float
+ max: Float
+ exclusiveMin: Float
+ exclusiveMax: Float
+ multipleOf: Float
+ oneOf: [Float]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 71c396c0..a51ed07d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -175,6 +175,18 @@ importers:
packages/fabrix:
dependencies:
+ '@hookform/resolvers':
+ specifier: ^3.9.0
+ version: 3.9.0(react-hook-form@7.53.0(react@18.3.1))
+ ajv:
+ specifier: ^8.17.1
+ version: 8.17.1
+ ajv-errors:
+ specifier: ^3.0.0
+ version: 3.0.0(ajv@8.17.1)
+ ajv-formats:
+ specifier: ^3.0.1
+ version: 3.0.1(ajv@8.17.1)
deepmerge-ts:
specifier: ^7.1.0
version: 7.1.3
@@ -235,7 +247,7 @@ importers:
version: 18.3.1
'@vitejs/plugin-react':
specifier: ^4.3.3
- version: 4.3.3(vite@5.4.6(@types/node@22.7.5))
+ version: 4.3.3(vite@5.4.8(@types/node@22.7.5))
eslint:
specifier: ^9.6.0
version: 9.12.0
@@ -256,7 +268,7 @@ importers:
version: 5.6.3
vite-tsconfig-paths:
specifier: ^4.3.2
- version: 4.3.2(typescript@5.6.3)(vite@5.4.6(@types/node@22.7.5))
+ version: 4.3.2(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5))
vitest:
specifier: ^2.0.3
version: 2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3))
@@ -311,7 +323,7 @@ importers:
version: 9.1.0(eslint@9.12.0)
eslint-plugin-import:
specifier: ^2.29.1
- version: 2.31.0(eslint@9.12.0)
+ version: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)
typescript:
specifier: ^5
version: 5.6.3
@@ -500,10 +512,6 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/runtime@7.25.6':
- resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==}
- engines: {node: '>=6.9.0'}
-
'@babel/runtime@7.25.7':
resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==}
engines: {node: '>=6.9.0'}
@@ -582,9 +590,6 @@ packages:
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
- '@emotion/is-prop-valid@1.3.0':
- resolution: {integrity: sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==}
-
'@emotion/is-prop-valid@1.3.1':
resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==}
@@ -918,10 +923,6 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
- '@eslint-community/regexpp@4.11.0':
- resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==}
- engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
'@eslint-community/regexpp@4.11.1':
resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -992,6 +993,11 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+ '@hookform/resolvers@3.9.0':
+ resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanfs/core@0.19.0':
resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==}
engines: {node: '>=18.18.0'}
@@ -1552,9 +1558,25 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ ajv-errors@3.0.0:
+ resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==}
+ peerDependencies:
+ ajv: ^8.0.1
+
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ ajv@8.17.1:
+ resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
@@ -1860,15 +1882,6 @@ packages:
supports-color:
optional: true
- debug@4.3.6:
- resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==}
- engines: {node: '>=6.0'}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
-
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
@@ -2131,6 +2144,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-uri@3.0.3:
+ resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==}
+
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@@ -2231,9 +2247,6 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- get-func-name@2.0.2:
- resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
-
get-intrinsic@1.2.4:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
@@ -2538,6 +2551,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -2599,9 +2615,6 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
- loupe@3.1.1:
- resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==}
-
loupe@3.1.2:
resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==}
@@ -2689,9 +2702,6 @@ packages:
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
- ms@2.1.2:
- resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
-
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2886,10 +2896,6 @@ packages:
yaml:
optional: true
- postcss@8.4.45:
- resolution: {integrity: sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==}
- engines: {node: ^10 || ^12 || >=14}
-
postcss@8.4.47:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -3046,6 +3052,10 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
@@ -3161,10 +3171,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
- source-map-js@1.2.0:
- resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
- engines: {node: '>=0.10.0'}
-
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -3562,37 +3568,6 @@ packages:
vite:
optional: true
- vite@5.4.6:
- resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==}
- engines: {node: ^18.0.0 || >=20.0.0}
- hasBin: true
- peerDependencies:
- '@types/node': ^18.0.0 || >=20.0.0
- less: '*'
- lightningcss: ^1.21.0
- sass: '*'
- sass-embedded: '*'
- stylus: '*'
- sugarss: '*'
- terser: ^5.4.0
- peerDependenciesMeta:
- '@types/node':
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- sass-embedded:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
-
vite@5.4.8:
resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -3967,10 +3942,6 @@ snapshots:
'@babel/core': 7.25.2
'@babel/helper-plugin-utils': 7.24.8
- '@babel/runtime@7.25.6':
- dependencies:
- regenerator-runtime: 0.14.1
-
'@babel/runtime@7.25.7':
dependencies:
regenerator-runtime: 0.14.1
@@ -4081,7 +4052,7 @@ snapshots:
'@emotion/babel-plugin@11.12.0':
dependencies:
'@babel/helper-module-imports': 7.24.7
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/serialize': 1.3.1
@@ -4114,20 +4085,15 @@ snapshots:
'@emotion/hash@0.9.2': {}
- '@emotion/is-prop-valid@1.3.0':
- dependencies:
- '@emotion/memoize': 0.9.0
-
'@emotion/is-prop-valid@1.3.1':
dependencies:
'@emotion/memoize': 0.9.0
- optional: true
'@emotion/memoize@0.9.0': {}
'@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1)':
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@emotion/babel-plugin': 11.12.0
'@emotion/cache': 11.13.1
'@emotion/serialize': 1.3.1
@@ -4153,9 +4119,9 @@ snapshots:
'@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)':
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@emotion/babel-plugin': 11.12.0
- '@emotion/is-prop-valid': 1.3.0
+ '@emotion/is-prop-valid': 1.3.1
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/serialize': 1.3.1
'@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1)
@@ -4322,8 +4288,6 @@ snapshots:
eslint: 9.12.0
eslint-visitor-keys: 3.4.3
- '@eslint-community/regexpp@4.11.0': {}
-
'@eslint-community/regexpp@4.11.1': {}
'@eslint/compat@1.2.0(eslint@9.12.0)':
@@ -4333,7 +4297,7 @@ snapshots:
'@eslint/config-array@0.18.0':
dependencies:
'@eslint/object-schema': 2.1.4
- debug: 4.3.6
+ debug: 4.3.7
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -4343,7 +4307,7 @@ snapshots:
'@eslint/eslintrc@3.1.0':
dependencies:
ajv: 6.12.6
- debug: 4.3.6
+ debug: 4.3.7
espree: 10.2.0
globals: 14.0.0
ignore: 5.3.2
@@ -4399,6 +4363,10 @@ snapshots:
dependencies:
graphql: 16.9.0
+ '@hookform/resolvers@3.9.0(react-hook-form@7.53.0(react@18.3.1))':
+ dependencies:
+ react-hook-form: 7.53.0(react@18.3.1)
+
'@humanfs/core@0.19.0': {}
'@humanfs/node@0.16.5':
@@ -4673,7 +4641,7 @@ snapshots:
'@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@testing-library/dom': 10.4.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -4894,17 +4862,6 @@ snapshots:
transitivePeerDependencies:
- graphql
- '@vitejs/plugin-react@4.3.3(vite@5.4.6(@types/node@22.7.5))':
- dependencies:
- '@babel/core': 7.25.2
- '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2)
- '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2)
- '@types/babel__core': 7.20.5
- react-refresh: 0.14.2
- vite: 5.4.6(@types/node@22.7.5)
- transitivePeerDependencies:
- - supports-color
-
'@vitejs/plugin-react@4.3.3(vite@5.4.8(@types/node@22.7.5))':
dependencies:
'@babel/core': 7.25.2
@@ -4923,14 +4880,14 @@ snapshots:
chai: 5.1.1
tinyrainbow: 1.2.0
- '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.6(@types/node@22.7.5))':
+ '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.8(@types/node@22.7.5))':
dependencies:
'@vitest/spy': 2.1.2
estree-walker: 3.0.3
magic-string: 0.30.11
optionalDependencies:
msw: 2.4.9(typescript@5.6.3)
- vite: 5.4.6(@types/node@22.7.5)
+ vite: 5.4.8(@types/node@22.7.5)
'@vitest/pretty-format@2.1.2':
dependencies:
@@ -4976,6 +4933,14 @@ snapshots:
acorn@8.12.1: {}
+ ajv-errors@3.0.0(ajv@8.17.1):
+ dependencies:
+ ajv: 8.17.1
+
+ ajv-formats@3.0.1(ajv@8.17.1):
+ optionalDependencies:
+ ajv: 8.17.1
+
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -4983,6 +4948,13 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ ajv@8.17.1:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.0.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
@@ -5172,7 +5144,7 @@ snapshots:
assertion-error: 2.0.1
check-error: 2.1.1
deep-eql: 5.0.2
- loupe: 3.1.1
+ loupe: 3.1.2
pathval: 2.0.0
chakra-react-select@5.0.1(@chakra-ui/react@2.10.2(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -5331,10 +5303,6 @@ snapshots:
dependencies:
ms: 2.1.3
- debug@4.3.6:
- dependencies:
- ms: 2.1.2
-
debug@4.3.7:
dependencies:
ms: 2.1.3
@@ -5565,16 +5533,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0):
dependencies:
debug: 3.2.7
optionalDependencies:
+ '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3)
eslint: 9.12.0
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(eslint@9.12.0):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -5585,7 +5554,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.12.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -5596,6 +5565,8 @@ snapshots:
semver: 6.3.1
string.prototype.trimend: 1.0.8
tsconfig-paths: 3.15.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -5635,7 +5606,7 @@ snapshots:
eslint@9.12.0:
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0)
- '@eslint-community/regexpp': 4.11.0
+ '@eslint-community/regexpp': 4.11.1
'@eslint/config-array': 0.18.0
'@eslint/core': 0.6.0
'@eslint/eslintrc': 3.1.0
@@ -5649,7 +5620,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
- debug: 4.3.6
+ debug: 4.3.7
escape-string-regexp: 4.0.0
eslint-scope: 8.1.0
eslint-visitor-keys: 4.1.0
@@ -5758,6 +5729,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fast-uri@3.0.3: {}
+
fastq@1.17.1:
dependencies:
reusify: 1.0.4
@@ -5853,8 +5826,6 @@ snapshots:
get-caller-file@2.0.5: {}
- get-func-name@2.0.2: {}
-
get-intrinsic@1.2.4:
dependencies:
es-errors: 1.3.0
@@ -6131,6 +6102,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
+ json-schema-traverse@1.0.0: {}
+
json-stable-stringify-without-jsonify@1.0.1: {}
json5@1.0.2:
@@ -6181,10 +6154,6 @@ snapshots:
dependencies:
js-tokens: 4.0.0
- loupe@3.1.1:
- dependencies:
- get-func-name: 2.0.2
-
loupe@3.1.2: {}
lru-cache@10.4.3: {}
@@ -6251,8 +6220,6 @@ snapshots:
ms@2.0.0: {}
- ms@2.1.2: {}
-
ms@2.1.3: {}
msw@2.4.9(typescript@5.6.3):
@@ -6423,18 +6390,11 @@ snapshots:
postcss: 8.4.47
tsx: 4.19.1
- postcss@8.4.45:
- dependencies:
- nanoid: 3.3.7
- picocolors: 1.1.0
- source-map-js: 1.2.0
-
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.0
source-map-js: 1.2.1
- optional: true
prelude-ls@1.2.1: {}
@@ -6601,6 +6561,8 @@ snapshots:
require-directory@2.1.1: {}
+ require-from-string@2.0.2: {}
+
requires-port@1.0.0: {}
resolve-from@4.0.0: {}
@@ -6751,10 +6713,7 @@ snapshots:
signal-exit@4.1.0: {}
- source-map-js@1.2.0: {}
-
- source-map-js@1.2.1:
- optional: true
+ source-map-js@1.2.1: {}
source-map@0.5.7: {}
@@ -6950,7 +6909,7 @@ snapshots:
cac: 6.7.14
chokidar: 3.6.0
consola: 3.2.3
- debug: 4.3.6
+ debug: 4.3.7
esbuild: 0.23.1
execa: 5.1.1
joycon: 3.1.1
@@ -7130,9 +7089,9 @@ snapshots:
vite-node@2.1.2(@types/node@22.7.5):
dependencies:
cac: 6.7.14
- debug: 4.3.6
+ debug: 4.3.7
pathe: 1.1.2
- vite: 5.4.6(@types/node@22.7.5)
+ vite: 5.4.8(@types/node@22.7.5)
transitivePeerDependencies:
- '@types/node'
- less
@@ -7150,30 +7109,21 @@ snapshots:
graphql-tag: 2.12.6(graphql@16.9.0)
magic-string: 0.30.11
- vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.6(@types/node@22.7.5)):
+ vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)):
dependencies:
debug: 4.3.7
globrex: 0.1.2
tsconfck: 3.1.3(typescript@5.6.3)
optionalDependencies:
- vite: 5.4.6(@types/node@22.7.5)
+ vite: 5.4.8(@types/node@22.7.5)
transitivePeerDependencies:
- supports-color
- typescript
- vite@5.4.6(@types/node@22.7.5):
- dependencies:
- esbuild: 0.21.5
- postcss: 8.4.45
- rollup: 4.21.2
- optionalDependencies:
- '@types/node': 22.7.5
- fsevents: 2.3.3
-
vite@5.4.8(@types/node@22.7.5):
dependencies:
esbuild: 0.21.5
- postcss: 8.4.45
+ postcss: 8.4.47
rollup: 4.21.2
optionalDependencies:
'@types/node': 22.7.5
@@ -7182,14 +7132,14 @@ snapshots:
vitest@2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3)):
dependencies:
'@vitest/expect': 2.1.2
- '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.6(@types/node@22.7.5))
+ '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.8(@types/node@22.7.5))
'@vitest/pretty-format': 2.1.2
'@vitest/runner': 2.1.2
'@vitest/snapshot': 2.1.2
'@vitest/spy': 2.1.2
'@vitest/utils': 2.1.2
chai: 5.1.1
- debug: 4.3.6
+ debug: 4.3.7
magic-string: 0.30.11
pathe: 1.1.2
std-env: 3.7.0
@@ -7197,7 +7147,7 @@ snapshots:
tinyexec: 0.3.0
tinypool: 1.0.1
tinyrainbow: 1.2.0
- vite: 5.4.6(@types/node@22.7.5)
+ vite: 5.4.8(@types/node@22.7.5)
vite-node: 2.1.2(@types/node@22.7.5)
why-is-node-running: 2.3.0
optionalDependencies: