diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 8a3f665..7dbc691 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,9 +10,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - actions: write # Necessary to cancel workflow executions - checks: write # Necessary to write reports +permissions: + actions: write # Necessary to cancel workflow executions + checks: write # Necessary to write reports pull-requests: write # Necessary to comment on PRs contents: read packages: write @@ -26,9 +26,7 @@ jobs: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup - - run: pnpm codegen - - name: Check source state - run: git add packages/*/src && git diff-index --cached HEAD --exit-code packages/*/src + - run: pnpm build types: name: Types @@ -51,13 +49,13 @@ jobs: - run: pnpm lint test: - name: Test + name: Test runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup - - run: pnpm vitest + - run: pnpm vitest env: NODE_OPTIONS: --max_old_space_size=8192 diff --git a/.vscode/settings.json b/.vscode/settings.json index 393a3c8..3c6ed85 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,7 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[typescriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "eslint.validate": ["markdown", "javascript", "typescript"], "editor.codeActionsOnSave": { diff --git a/package.json b/package.json index b970ea2..f4df9ef 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ ], "scripts": { "clean": "node scripts/clean.mjs", - "codegen": "pnpm --recursive --parallel run codegen && pnpm lint-fix", "build": "tsc -b tsconfig.build.json && pnpm --recursive --parallel run build", "check": "tsc -b tsconfig.json", "check-recursive": "pnpm --recursive exec tsc -b tsconfig.json", diff --git a/packages/cli/package.json b/packages/cli/package.json index ee22df3..ce2aff8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,7 +14,6 @@ "directory": "dist" }, "scripts": { - "codegen": "build-utils prepare-v2", "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", "build-esm": "tsc -b tsconfig.build.json", "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", diff --git a/packages/core/package.json b/packages/core/package.json index 69c7632..ccf5313 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,18 +3,17 @@ "version": "0.0.1", "type": "module", "license": "MIT", - "description": "The server template", + "description": "The core package", "repository": { "type": "git", "url": "Inato SAS", - "directory": "packages/server" + "directory": "packages/core" }, "publishConfig": { "access": "public", "directory": "dist" }, "scripts": { - "codegen": "build-utils prepare-v2", "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", "build-esm": "tsc -b tsconfig.build.json", "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", @@ -41,9 +40,13 @@ } }, "devDependencies": { - "effect": "^3.11.6" + "@types/react": "^19.0.1", + "effect": "^3.11.6", + "react": "^19.0.0", + "react-hook-form": "^7.54.1" }, "peerDependencies": { - "effect": "^3.11.6" + "effect": "^3.11.6", + "react": "^18" } } diff --git a/packages/core/src/FormBody.ts b/packages/core/src/FormBody.ts index 9e19ee2..276ddd0 100644 --- a/packages/core/src/FormBody.ts +++ b/packages/core/src/FormBody.ts @@ -1,131 +1,127 @@ -import type { Types } from 'effect'; -import { Predicate, Schema, Tuple } from 'effect'; +import type { Types } from "effect" +import { Predicate, Schema, Tuple } from "effect" -import type { FormField } from './FormField.js'; +import type * as FormField from "./FormField.js" -type FormSchemaFields = Types.Simplify<{ - [key in keyof Fields]: Fields[key] extends FormBody.Any - ? FormSchema - : Fields[key]['schema']; -}>; +type FormSchemaFields = Types.Simplify< + { + [key in keyof Fields]: Fields[key] extends Any ? FormSchema + : Fields[key]["schema"] + } +> -type FormSchemaStruct = T extends Schema.Struct.Fields - ? Schema.Struct - : never; +type FormSchemaStruct = T extends Schema.Struct.Fields ? Schema.Struct + : never -type FormSchema = FormSchemaStruct< +type FormSchema = FormSchemaStruct< FormSchemaFields ->; +> -export const FormStructTypeId = Symbol.for('@inato/Form/FormBody/FormStruct'); -export type FormStructTypeId = typeof FormStructTypeId; +export const FormStructTypeId = Symbol.for("@inato/Form/FormBody/FormStruct") +export type FormStructTypeId = typeof FormStructTypeId -const isFormStruct = ( - value: unknown, -): value is FormStruct => - Predicate.hasProperty(value, FormStructTypeId); +export const isFormStruct = ( + value: unknown +): value is FormStruct => Predicate.hasProperty(value, FormStructTypeId) -interface FormStruct { - [FormStructTypeId]: FormStructTypeId; - fields: Fields; - schema: FormSchema; - defaultValue: FormSchema['Encoded']; +interface FormStruct { + [FormStructTypeId]: FormStructTypeId + fields: Fields + schema: FormSchema + defaultValue: FormSchema["Encoded"] } -export const FormArrayTypeId = Symbol.for('@inato/Form/FormBody/FormArray'); -export type FormArrayTypeId = typeof FormArrayTypeId; +export const FormArrayTypeId = Symbol.for("@inato/Form/FormBody/FormArray") +export type FormArrayTypeId = typeof FormArrayTypeId -const isFormArray = (value: unknown): value is FormBody.AnyArray => - Predicate.hasProperty(value, FormArrayTypeId); +export const isFormArray = (value: unknown): value is AnyArray => Predicate.hasProperty(value, FormArrayTypeId) -interface FormArray { - [FormArrayTypeId]: FormArrayTypeId; - field: Field; - schema: Schema.Array$; - defaultValue: FormArray['schema']['Encoded']; +interface FormArray { + [FormArrayTypeId]: FormArrayTypeId + field: Field + schema: Schema.Array$ + defaultValue: FormArray["schema"]["Encoded"] } -export const FormMapTypeId = Symbol.for('@inato/Form/FormBody/FormMap'); -export type FormMapTypeId = typeof FormMapTypeId; +export const FormMapTypeId = Symbol.for("@inato/Form/FormBody/FormMap") +export type FormMapTypeId = typeof FormMapTypeId -const isFormMap = (value: unknown): value is FormBody.AnyMap => - Predicate.hasProperty(value, FormMapTypeId); +export const isFormMap = (value: unknown): value is AnyMap => Predicate.hasProperty(value, FormMapTypeId) interface FormMap< Key extends Schema.Schema.AnyNoContext, - Field extends FormBody.AnyNonIterableField, + Field extends AnyNonIterableField > { - [FormMapTypeId]: FormMapTypeId; - field: Field; - keySchema: Key; - schema: Schema.HashMap; - defaultValue: FormMap['schema']['Encoded']; + [FormMapTypeId]: FormMapTypeId + field: Field + keySchema: Key + schema: Schema.HashMap + defaultValue: FormMap["schema"]["Encoded"] defaultValueFor: ( - keys: ReadonlyArray, - ) => FormMap['defaultValue']; + keys: ReadonlyArray + ) => FormMap["defaultValue"] } -export const FormRawTypeId = Symbol.for('@inato/Form/FormBody/FormRaw'); -export type FormRawTypeId = typeof FormRawTypeId; +export const FormRawTypeId = Symbol.for("@inato/Form/FormBody/FormRaw") +export type FormRawTypeId = typeof FormRawTypeId -const isFormRaw = (value: unknown): value is FormBody.AnyRaw => - Predicate.hasProperty(value, FormRawTypeId); +export const isFormRaw = (value: unknown): value is AnyRaw => Predicate.hasProperty(value, FormRawTypeId) interface FormRaw { - [FormRawTypeId]: FormRawTypeId; - schema: S; - defaultValue: S['Encoded']; + [FormRawTypeId]: FormRawTypeId + schema: S + defaultValue: S["Encoded"] } -const makeStructSchema = ( - fields: Fields, +const makeStructSchema = ( + fields: Fields ): { - schema: FormSchema; - defaultValue: FormSchema['Encoded']; + schema: FormSchema + defaultValue: FormSchema["Encoded"] } => { - const schemaFields: Types.Mutable = {}; - const defaultValue: Record = {}; + const schemaFields: Types.Mutable = {} + const defaultValue: Record = {} for (const [key, field] of Object.entries(fields)) { - schemaFields[key] = field.schema; - if ('matchDefaultValue' in field) { + schemaFields[key] = field.schema + if ("matchDefaultValue" in field) { field.matchDefaultValue({ - withDefaultValue: value => { - defaultValue[key] = value; - }, - }); + withDefaultValue: (value) => { + defaultValue[key] = value + } + }) } else { - defaultValue[key] = field.defaultValue; + defaultValue[key] = field.defaultValue } } // @ts-expect-error "structSchema is indeed of type FormSchema" - const structSchema: FormSchema = Schema.Struct(schemaFields); - return { schema: structSchema, defaultValue }; -}; + const structSchema: FormSchema = Schema.Struct(schemaFields) + return { schema: structSchema, defaultValue } +} -const struct = ( - fields: Fields, +export const struct = ( + fields: Fields ): FormStruct => { - const { schema, defaultValue } = makeStructSchema(fields); - return { [FormStructTypeId]: FormStructTypeId, fields, schema, defaultValue }; -}; + const { defaultValue, schema } = makeStructSchema(fields) + return { [FormStructTypeId]: FormStructTypeId, fields, schema, defaultValue } +} -const array = ( - field: Field, +export const array = ( + field: Field ): FormArray => { return { [FormArrayTypeId]: FormArrayTypeId, field, schema: Schema.Array(field.schema), - defaultValue: [], - }; -}; + defaultValue: [] + } +} -const map = ({ - key, +export const map = ({ field, + key }: { - key: Schema.Schema; - field: Field; + key: Schema.Schema + field: Field }): FormMap, Field> => { return { [FormMapTypeId]: FormMapTypeId, @@ -134,56 +130,42 @@ const map = ({ schema: Schema.HashMap({ key, value: field.schema }), defaultValue: [], defaultValueFor(keys) { - const defaultValue: typeof field.schema.Encoded = - 'getDefaultValue' in field - ? field.getDefaultValue() - : field.defaultValue; - return keys.map(key => Tuple.make(key, defaultValue)); - }, - }; -}; - -const raw = ({ - schema, + const defaultValue: typeof field.schema.Encoded = "getDefaultValue" in field + ? field.getDefaultValue() + : field.defaultValue + return keys.map((key) => Tuple.make(key, defaultValue)) + } + } +} + +export const raw = ({ defaultValue, + schema }: { - schema: S; - defaultValue: S['Encoded']; + schema: S + defaultValue: S["Encoded"] }): FormRaw => { return { [FormRawTypeId]: FormRawTypeId, schema, - defaultValue, - }; -}; - -export declare namespace FormBody { - export type AnyNonIterableField = Any | FormField.Any; + defaultValue + } +} - export type AnyArray = FormArray; +export type AnyNonIterableField = Any | FormField.Any - export type AnyMap = FormMap; +export type AnyArray = FormArray - export type AnyRaw = FormRaw; +export type AnyMap = FormMap - export type AnyIterable = - | FormArray - | FormMap; +export type AnyRaw = FormRaw - export type AnyField = AnyNonIterableField | AnyIterable | AnyRaw; +export type AnyIterable = + | FormArray + | FormMap - export type AnyFields = Record; +export type AnyField = AnyNonIterableField | AnyIterable | AnyRaw - export type Any = FormStruct; -} +export type AnyFields = Record -export const FormBody = { - struct, - isFormStruct, - raw, - isFormRaw, - array, - isFormArray, - map, - isFormMap, -}; +export type Any = FormStruct diff --git a/packages/core/src/FormDisplay.ts b/packages/core/src/FormDisplay.ts index e762c59..fbaf422 100644 --- a/packages/core/src/FormDisplay.ts +++ b/packages/core/src/FormDisplay.ts @@ -1,170 +1,156 @@ -import type { Context, Types } from 'effect'; -import { Effect, Function, Option } from 'effect'; +import type { Context, Types } from "effect" +import { Effect, Function, Option } from "effect" -import { FormBody } from './FormBody.js'; -import type { FormField } from './FormField.js'; -import { FormFramework } from './FormFramework.js'; -import { Path } from './Path.js'; +import * as FormBody from "./FormBody.js" +import type * as FormField from "./FormField.js" +import * as FormFramework from "./FormFramework.js" +import { Path } from "./Path.js" -interface ArrayDisplay - extends FormFramework.MakeIterable { - Element: FormDisplay.AnyFieldDisplay; +interface ArrayDisplay extends FormFramework.MakeIterable { + Element: AnyFieldDisplay } -interface MapDisplay - extends FormFramework.MakeIterable { - Element: FormDisplay.AnyFieldDisplay & - FormFramework.MakeMapKey; +interface MapDisplay extends FormFramework.MakeIterable { + Element: + & AnyFieldDisplay + & FormFramework.MakeMapKey } -interface RawDisplay - extends FormFramework.MakeRaw {} - -type FieldDisplay = ReturnType< - Context.Tag.Service -> & { - useControls: () => FormFramework.FieldControls; -}; - -type FormDisplay = { - [key in keyof Body['fields']]: FormDisplay.AnyFieldDisplay< - Body['fields'][key] - >; -} & { - useControls: () => FormFramework.FieldControls; -}; - -type AnyFieldDependencies = - Field extends FormField.Any - ? Field['tag'] - : Field extends FormBody.Any - ? FormDisplayDependenciesTag - : Field extends FormBody.AnyIterable - ? AnyFieldDependencies - : never; +type RawDisplay = FormFramework.MakeRaw + +type FieldDisplay = + & ReturnType< + Context.Tag.Service + > + & { + useControls: () => FormFramework.FieldControls + } + +type FormDisplay = + & { + [key in keyof Body["fields"]]: AnyFieldDisplay< + Body["fields"][key] + > + } + & { + useControls: () => FormFramework.FieldControls + } + +type AnyFieldDependencies = Field extends FormField.Any ? Field["tag"] + : Field extends FormBody.Any ? FormDisplayDependenciesTag + : Field extends FormBody.AnyIterable ? AnyFieldDependencies + : never type FormDisplayDependenciesTag = { - [key in keyof Body['fields']]: AnyFieldDependencies; -}[keyof Body['fields']]; + [key in keyof Body["fields"]]: AnyFieldDependencies +}[keyof Body["fields"]] -type FormDisplayDependencies = - Context.Tag.Identifier>; +type FormDisplayDependencies = Context.Tag.Identifier> const addControls = (component: T, path: Path) => - Effect.gen(function* () { - const { makeFieldControls } = yield* FormFramework; + Effect.gen(function*() { + const { makeFieldControls } = yield* FormFramework.FormFramework // @ts-expect-error "'T' could be instantiated with an arbitrary type" const res: T & { - useControls: () => FormFramework.FieldControls; + useControls: () => FormFramework.FieldControls } = Object.assign( // @ts-expect-error "Argument of type 'T' is not assignable to parameter of type 'object'" component, - makeFieldControls(path), - ); - return res; - }); + makeFieldControls(path) + ) + return res + }) const makeField = (field: FormField.Any, path: Path) => { - return Effect.gen(function* () { - const builder = yield* field.tag; - const component = builder({ path }); - return yield* addControls(component, path); - }); -}; + return Effect.gen(function*() { + const builder = yield* field.tag + const component = builder({ path }) + return yield* addControls(component, path) + }) +} const makeImpl = ( body: Body, - path: Path = Path.empty, + path: Path = Path.empty ): Effect.Effect< Types.Simplify>, never, FormDisplayDependencies > => { - return Effect.gen(function* () { - const framework = yield* FormFramework; + return Effect.gen(function*() { + const framework = yield* FormFramework.FormFramework // @ts-expect-error "{} is incompatible with the desired type" - const display: Types.Simplify> = {}; + const display: Types.Simplify> = {} for (const [key, fieldValue] of Object.entries(body.fields)) { - const currentPath = path.appendString(key); + const currentPath = path.appendString(key) if (FormBody.isFormStruct(fieldValue)) { // @ts-expect-error "key does not exist in display" - display[key] = yield* makeImpl(fieldValue, currentPath); + display[key] = yield* makeImpl(fieldValue, currentPath) } else if ( FormBody.isFormArray(fieldValue) || FormBody.isFormMap(fieldValue) ) { - const { field } = fieldValue; - const defaultValue = - 'getDefaultValue' in field - ? Option.getOrUndefined(field.getDefaultValue()) - : field.defaultValue; - const fieldArray = framework.makeIterable(defaultValue, currentPath); + const { field } = fieldValue + const defaultValue = "getDefaultValue" in field + ? Option.getOrUndefined(field.getDefaultValue()) + : field.defaultValue + const fieldArray = framework.makeIterable(defaultValue, currentPath) const pathWithIndex = FormBody.isFormMap(fieldValue) - ? currentPath.appendIndex().appendString('1') - : currentPath.appendIndex(); + ? currentPath.appendIndex().appendString("1") + : currentPath.appendIndex() const Element = FormBody.isFormStruct(field) ? yield* makeImpl(field, pathWithIndex) - : yield* makeField(field, pathWithIndex); + : yield* makeField(field, pathWithIndex) Object.assign(fieldArray, { - Element, - }); + Element + }) if (FormBody.isFormMap(fieldValue)) { Object.assign( Element, framework.makeMapKey( fieldValue.keySchema, - currentPath.appendIndex().appendString('0'), - ), - ); + currentPath.appendIndex().appendString("0") + ) + ) } // @ts-expect-error "key does not exist in display" - display[key] = fieldArray; + display[key] = fieldArray } else if (FormBody.isFormRaw(fieldValue)) { // @ts-expect-error "key does not exist in display" - display[key] = framework.makeRaw(currentPath); + display[key] = framework.makeRaw(currentPath) } else { // @ts-expect-error "key does not exist in display" - display[key] = yield* makeField(fieldValue, currentPath); + display[key] = yield* makeField(fieldValue, currentPath) } } - return yield* addControls(display, path); - }); -}; - -export declare namespace FormDisplay { - export type AnyFieldDisplay = - Field extends FormField.Any - ? FieldDisplay - : Field extends FormBody.Any - ? Types.Simplify> - : Field extends FormBody.AnyArray - ? ArrayDisplay - : Field extends FormBody.AnyMap - ? MapDisplay - : Field extends FormBody.AnyRaw - ? RawDisplay - : never; + return yield* addControls(display, path) + }) } +export type AnyFieldDisplay = Field extends FormField.Any ? FieldDisplay + : Field extends FormBody.Any ? Types.Simplify> + : Field extends FormBody.AnyArray ? ArrayDisplay + : Field extends FormBody.AnyMap ? MapDisplay + : Field extends FormBody.AnyRaw ? RawDisplay + : never + // we must add & {} in the type so that Object.assign works correctly -const makeObjectAssignable = (value: T) => - Function.unsafeCoerce(value); +const makeObjectAssignable = (value: T) => Function.unsafeCoerce(value) const make = (body: Body) => { - return Effect.gen(function* () { - const display = makeObjectAssignable(yield* makeImpl(body)); - const framework = yield* FormFramework; - const Form: FormFramework.FormComponent = - framework.makeForm({ - schema: body.schema, - resetValues: body.defaultValue, - }); - const Submit = framework.makeSubmit(Form.id); - const Clear = framework.Clear; - return Object.assign(display, { Form, Submit, Clear }); - }); -}; + return Effect.gen(function*() { + const display = makeObjectAssignable(yield* makeImpl(body)) + const framework = yield* FormFramework.FormFramework + const Form: FormFramework.FormComponent = framework.makeForm({ + schema: body.schema, + resetValues: body.defaultValue + }) + const Submit = framework.makeSubmit(Form.id) + const Clear = framework.Clear + return Object.assign(display, { Form, Submit, Clear }) + }) +} export const FormDisplay = { - make, -}; + make +} diff --git a/packages/core/src/FormField.ts b/packages/core/src/FormField.ts index 208a453..c77879f 100644 --- a/packages/core/src/FormField.ts +++ b/packages/core/src/FormField.ts @@ -1,122 +1,118 @@ -import type { Schema } from 'effect'; -import { Context, Effect, Layer, Option } from 'effect'; -import type React from 'react'; +import type { Schema } from "effect" +import { Context, Effect, Layer, Option } from "effect" +import type React from "react" -import type { Path } from './Path.js'; +import type { Path } from "./Path.js" -const NoDefaultValue = Symbol.for('FormField/NoDefaultValue'); -type NoDefaultValue = typeof NoDefaultValue; +const NoDefaultValue = Symbol.for("FormField/NoDefaultValue") +type NoDefaultValue = typeof NoDefaultValue class FormFieldClass< Self, A extends React.FC, - S extends Schema.Schema.AnyNoContext, + S extends Schema.Schema.AnyNoContext > { private constructor( - readonly tag: Context.Tag>, + readonly tag: Context.Tag>, readonly schema: S, - private readonly defaultValue: S['Encoded'] | NoDefaultValue, + private readonly defaultValue: S["Encoded"] | NoDefaultValue ) {} static withDefaultValue = < Self, A extends React.FC, - S extends Schema.Schema.AnyNoContext, + S extends Schema.Schema.AnyNoContext >( - tag: Context.Tag>, + tag: Context.Tag>, schema: S, - defaultValue: S['Encoded'] | NoDefaultValue, - ) => new FormFieldClass(tag, schema, defaultValue); + defaultValue: S["Encoded"] | NoDefaultValue + ) => new FormFieldClass(tag, schema, defaultValue) static withoutDefaultValue = < Self, A extends React.FC, - S extends Schema.Schema.AnyNoContext, + S extends Schema.Schema.AnyNoContext >( - tag: Context.Tag>, - schema: S, - ) => new FormFieldClass(tag, schema, NoDefaultValue); + tag: Context.Tag>, + schema: S + ) => new FormFieldClass(tag, schema, NoDefaultValue) decorate(): FormFieldClass { - //@ts-expect-error "casting this to another ReactFC type" - return this; + // @ts-expect-error "casting this to another ReactFC type" + return this } - getDefaultValue(): Option.Option { + getDefaultValue(): Option.Option { return Option.liftPredicate( this.defaultValue, - value => value !== NoDefaultValue, - ); + (value) => value !== NoDefaultValue + ) } matchDefaultValue({ withDefaultValue, - withoutDefaultValue, + withoutDefaultValue }: { - withDefaultValue: (value: S['Encoded']) => void; - withoutDefaultValue?: () => void; + withDefaultValue: (value: S["Encoded"]) => void + withoutDefaultValue?: () => void }): void { if (this.defaultValue === NoDefaultValue) { - withoutDefaultValue?.(); + withoutDefaultValue?.() } else { - withDefaultValue(this.defaultValue); + withDefaultValue(this.defaultValue) } } } -export declare namespace FormField { - export interface ComponentBuilder> { - (_: { path: Path }): A; - } +export interface ComponentBuilder> { + (_: { path: Path }): A +} - export type OfProps = FormFieldClass< - any, - React.FC, - Schema.Schema.AnyNoContext - >; +export type OfProps = FormFieldClass< + any, + React.FC, + Schema.Schema.AnyNoContext +> - export type Any = FormFieldClass< - any, - React.FC, - Schema.Schema.AnyNoContext - >; -} +export type Any = FormFieldClass< + any, + React.FC, + Schema.Schema.AnyNoContext +> -export const FormField = - (id: Id) => - < - Self, - A extends React.FC = React.FC, - S_ extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext, - >() => { - const tag = Context.Tag(id)>(); - return Object.assign(tag, { - make: (props: { - schema: S; - defaultValue: S['Encoded']; - }): FormFieldClass => - FormFieldClass.withDefaultValue(tag, props.schema, props.defaultValue), - makeRequired: (props: { - schema: S; - }): FormFieldClass => - FormFieldClass.withoutDefaultValue( - tag, - // @ts-expect-error "schema.annotations looses the type" - props.schema.annotations({ - message: () => ({ - message: 'This field is required', - override: true, - }), - }), - ), - layerBuilder: ( - component: - | FormField.ComponentBuilder - | Effect.Effect, E, R>, - ): Layer.Layer => { - if (Effect.isEffect(component)) { - return Layer.effect(tag, component); - } - return Layer.succeed(tag, component); - }, - }); - }; +export const FormField = (id: Id) => +< + Self, + A extends React.FC = React.FC, + S_ extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext +>() => { + const tag = Context.Tag(id)>() + return Object.assign(tag, { + make: (props: { + schema: S + defaultValue: S["Encoded"] + }): FormFieldClass => FormFieldClass.withDefaultValue(tag, props.schema, props.defaultValue), + makeRequired: (props: { + schema: S + }): FormFieldClass => + FormFieldClass.withoutDefaultValue( + tag, + // @ts-expect-error "schema.annotations looses the type" + props.schema.annotations({ + message: () => ({ + message: "This field is required", + override: true + }) + }) + ), + layerBuilder: ( + component: + | ComponentBuilder + | Effect.Effect, E, R> + ): Layer.Layer => { + if (Effect.isEffect(component)) { + return Layer.effect(tag, component) + } + return Layer.succeed(tag, component) + } + }) +} diff --git a/packages/core/src/FormFramework.ts b/packages/core/src/FormFramework.ts new file mode 100644 index 0000000..63e45fe --- /dev/null +++ b/packages/core/src/FormFramework.ts @@ -0,0 +1,157 @@ +import { Context, Either, ParseResult, Predicate, Schema } from "effect" +import type React from "react" +import type { FieldPath } from "react-hook-form" + +import type * as FormBody from "./FormBody.js" +import type { Path } from "./Path.js" + +/** + * FormFramework + */ + +type Values = + | { + encoded: S["Encoded"] | ((from: S["Encoded"]) => S["Encoded"]) + } + | { unknown: unknown } + +const getValues = (schema: S) => +({ + defaultValues, + values +}: { + values: Values> | undefined + defaultValues: NoInfer["Encoded"] +}): S["Encoded"] => { + if (!values) return defaultValues + if ("encoded" in values) { + if (Predicate.isFunction(values.encoded)) { + return values.encoded(defaultValues) + } else { + return values.encoded + } + } else { + const encodedSchema = Schema.encodedSchema(schema) + const either = Schema.decodeUnknownEither(encodedSchema, { + errors: "all" + })(values.unknown) + if (Either.isRight(either)) { + return either.right + } else { + // TODO: use effect log + + console.log( + "[warning] Provided values are not valid. Falling back on default values", + ParseResult.ArrayFormatter.formatErrorSync(either.left) + ) + return defaultValues + } + } +} + +export class FormFramework extends Context.Tag("@inato/Form/FormFramework")< + FormFramework, + IFormFramework +>() { + static getValues = getValues +} + +export interface FormComponentProps { + children: React.ReactNode + onSubmit: (_: { + decoded: S["Type"] + encoded: S["Encoded"] + }) => void | Promise + onError?: (values: unknown) => void + /** + * The validation mode. Default: 'onBlur' + */ + validationMode?: "onBlur" | "onSubmit" | "onChange" + /** + * The values to hydrate the form with, typically loading some values already saved by the user + */ + initialValues?: Values + /** + * The starting point of an empty form + */ + resetValues?: Values +} +export interface FormComponent extends React.FC> { + /** + * The form id to synchronize with Submit button + */ + id: string +} + +export type ReactFCWithChildren = React.FC<{ + children: React.ReactNode +}> + +export interface FieldControls< + Field extends FormBody.AnyField = FormBody.AnyField +> { + watch: () => Field["schema"]["Encoded"] + reset: () => void + set: (value: Field["schema"]["Encoded"]) => void +} + +export interface ArrayControls< + Field extends FormBody.AnyArray = FormBody.AnyArray +> extends FieldControls { + append: (value?: Field["field"]["schema"]["Encoded"]) => void + useRemove: () => { remove: () => void } +} + +export interface MakeIterable< + Field extends FormBody.AnyIterable = FormBody.AnyIterable +> extends ReactFCWithChildren { + Fields: ReactFCWithChildren + useControls: () => Field extends FormBody.AnyArray ? ArrayControls + : FieldControls +} + +export interface RawControls extends FieldControls { + usePath: >( + path: T + ) => string +} + +export interface MakeRaw extends ReactFCWithChildren { + useControls: () => RawControls +} + +export interface MakeMapKey { + Key: React.FC + useKey: () => S["Encoded"] +} + +export type Button = React.FC<{ variant: string; loading: boolean }> + +export interface IFormFramework { + makeFieldControls: (path: Path) => { + useControls: () => FieldControls + } + + makeIterable: ( + defaultValue: unknown, + path: Path + ) => MakeIterable + + makeMapKey: ( + schema: S, + path: Path + ) => MakeMapKey + + makeRaw: (path: Path) => MakeRaw + + makeForm: (_: { + schema: S + resetValues: S["Encoded"] + }) => FormComponent + + makeSubmit: (formId: string) => Button + + useError: (path: Path) => T + + Clear: Button +} diff --git a/packages/core/src/FormFramework.tsx b/packages/core/src/FormFramework.tsx deleted file mode 100644 index 6db9603..0000000 --- a/packages/core/src/FormFramework.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import type { ButtonProps } from '@inato/ui'; -import { Context, Either, ParseResult, Predicate, Schema } from 'effect'; -import type React from 'react'; -import type { FieldPath } from 'react-hook-form'; - -import type { FormBody } from './FormBody.js'; -import type { Path } from './Path.js'; - -type Values = - | { - encoded: S['Encoded'] | ((from: S['Encoded']) => S['Encoded']); - } - | { unknown: unknown }; - -const getValues = - (schema: S) => - ({ - defaultValues, - values, - }: { - values: Values> | undefined; - defaultValues: NoInfer['Encoded']; - }): S['Encoded'] => { - if (!values) return defaultValues; - if ('encoded' in values) { - if (Predicate.isFunction(values.encoded)) { - return values.encoded(defaultValues); - } else { - return values.encoded; - } - } else { - const encodedSchema = Schema.encodedSchema(schema); - const either = Schema.decodeUnknownEither(encodedSchema, { - errors: 'all', - })(values.unknown); - if (Either.isRight(either)) { - return either.right; - } else { - // TODO: use effect log - // eslint-disable-next-line no-console - console.log( - '[warning] Provided values are not valid. Falling back on default values', - ParseResult.ArrayFormatter.formatErrorSync(either.left), - ); - return defaultValues; - } - } - }; - -export declare namespace FormFramework { - export interface FormComponentProps { - children: React.ReactNode; - onSubmit: (_: { - decoded: S['Type']; - encoded: S['Encoded']; - }) => void | Promise; - onError?: (values: unknown) => void; - /** - * The validation mode. Default: 'onBlur' - */ - validationMode?: 'onBlur' | 'onSubmit' | 'onChange'; - /** - * The values to hydrate the form with, typically loading some values already saved by the user - */ - initialValues?: Values; - /** - * The starting point of an empty form - */ - resetValues?: Values; - } - export interface FormComponent - extends React.FC> { - /** - * The form id to synchronize with Submit button - */ - id: string; - } - - export interface ReactFCWithChildren - extends React.FC<{ - children: React.ReactNode; - }> {} - - export interface FieldControls< - Field extends FormBody.AnyField = FormBody.AnyField, - > { - watch: () => Field['schema']['Encoded']; - reset: () => void; - set: (value: Field['schema']['Encoded']) => void; - } - - export interface ArrayControls< - Field extends FormBody.AnyArray = FormBody.AnyArray, - > extends FieldControls { - append: (value?: Field['field']['schema']['Encoded']) => void; - useRemove: () => { remove: () => void }; - } - - export interface MakeIterable< - Field extends FormBody.AnyIterable = FormBody.AnyIterable, - > extends ReactFCWithChildren { - Fields: ReactFCWithChildren; - useControls: () => Field extends FormBody.AnyArray - ? ArrayControls - : FieldControls; - } - - export interface RawControls - extends FieldControls { - usePath: >( - path: T, - ) => string; - } - - export interface MakeRaw - extends ReactFCWithChildren { - useControls: () => RawControls; - } - - export interface MakeMapKey { - Key: React.FC; - useKey: () => S['Encoded']; - } - - export interface Button extends React.FC> {} - - export interface IFormFramework { - makeFieldControls: (path: Path) => { - useControls: () => FieldControls; - }; - - makeIterable: ( - defaultValue: unknown, - path: Path, - ) => MakeIterable; - - makeMapKey: ( - schema: S, - path: Path, - ) => MakeMapKey; - - makeRaw: (path: Path) => MakeRaw; - - makeForm: (_: { - schema: S; - resetValues: S['Encoded']; - }) => FormComponent; - - makeSubmit: (formId: string) => Button; - - useError: (path: Path) => T; - - Clear: Button; - } -} - -export class FormFramework extends Context.Tag('@inato/Form/FormFramework')< - FormFramework, - FormFramework.IFormFramework ->() { - static getValues = getValues; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 587442d..47903fa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,5 @@ -export { FormBody } from './FormBody.js'; -export { FormDisplay } from './FormDisplay.js'; -export { FormField } from './FormField.js'; -export { FormFramework } from './FormFramework.js'; -export { Path } from './Path.js'; - +export * as FormBody from "./FormBody.js" +export * as FormDisplay from "./FormDisplay.js" +export * as FormField from "./FormField.js" +export * as FormFramework from "./FormFramework.js" +export * as Path from "./Path.jsx" diff --git a/packages/domain/package.json b/packages/domain/package.json index 889dab1..556948c 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -14,7 +14,6 @@ "directory": "dist" }, "scripts": { - "codegen": "build-utils prepare-v2", "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", "build-esm": "tsc -b tsconfig.build.json", "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ee40f0..d5db2e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,9 +138,18 @@ importers: specifier: workspace:^ version: link:../domain/dist devDependencies: + '@types/react': + specifier: ^19.0.1 + version: 19.0.1 effect: specifier: ^3.11.6 version: 3.11.6 + react: + specifier: ^19.0.0 + version: 19.0.0 + react-hook-form: + specifier: ^7.54.1 + version: 7.54.1(react@19.0.0) publishDirectory: dist packages/domain: @@ -1095,6 +1104,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/react@19.0.1': + resolution: {integrity: sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1373,6 +1385,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -2412,9 +2427,19 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-hook-form@7.54.1: + resolution: {integrity: sha512-PUNzFwQeQ5oHiiTUO7GO/EJXGEtuun2Y1A59rLnZBBj+vNEOWt/3ERTiG1/zt7dVeJEM+4vDX/7XQ/qanuvPMg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -3747,6 +3772,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/react@19.0.1': + dependencies: + csstype: 3.1.3 + '@types/stack-utils@2.0.3': {} '@types/unist@2.0.11': {} @@ -4081,6 +4110,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.1.3: {} + data-view-buffer@1.0.1: dependencies: call-bind: 1.0.8 @@ -5256,8 +5287,14 @@ snapshots: queue-microtask@1.2.3: {} + react-hook-form@7.54.1(react@19.0.0): + dependencies: + react: 19.0.0 + react-is@18.3.1: {} + react@19.0.0: {} + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 diff --git a/tsconfig.base.json b/tsconfig.base.json index 17490c1..09036b6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,7 @@ "moduleResolution": "NodeNext", "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": [], + "jsx": "react", "isolatedModules": true, "sourceMap": true, "declarationMap": true, @@ -43,9 +44,9 @@ "@inato-form/fields": ["./packages/domain/src/index.js"], "@inato-form/fields/*": ["./packages/domain/src/*.js"], "@inato-form/fields/test/*": ["./packages/domain/test/*.js"], - "@inato-form/core": ["./packages/server/src/index.js"], - "@inato-form/core/*": ["./packages/server/src/*.js"], - "@inato-form/core/test/*": ["./packages/server/test/*.js"] + "@inato-form/core": ["./packages/core/src/index.js"], + "@inato-form/core/*": ["./packages/core/src/*.js"], + "@inato-form/core/test/*": ["./packages/core/test/*.js"] } } } diff --git a/tsconfig.build.json b/tsconfig.build.json index fb19e9f..191fe2d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,6 +4,6 @@ "references": [ { "path": "packages/cli/tsconfig.build.json" }, { "path": "packages/domain/tsconfig.build.json" }, - { "path": "packages/server/tsconfig.build.json" } + { "path": "packages/core/tsconfig.build.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index 4e4256c..26ffd13 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,6 @@ "references": [ { "path": "packages/cli" }, { "path": "packages/domain" }, - { "path": "packages/server" } + { "path": "packages/core" } ] }