From 8fb90e2e37a206e49c3ef5453bbb7a88725b1cdc Mon Sep 17 00:00:00 2001 From: Shai Reznik Date: Tue, 26 Nov 2024 18:14:59 +0200 Subject: [PATCH 1/3] added `routeFormAction$` and `globalFormAction$` and deprecated `formAction$` --- .github/workflows/pre-release.yml | 42 ++++ packages/qwik/src/actions/formAction$.ts | 299 ++++++++++++++++------- 2 files changed, 248 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/pre-release.yml diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 00000000..6368d0ce --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,42 @@ +name: Prerelease + +on: + pull_request: + +jobs: + prerelease: + runs-on: ubuntu-latest + steps: + - name: Checkout all commits + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.7.0 + + - name: Use Node + uses: actions/setup-node@v4 + with: + node-version: 22 + # This doesn't just set the registry url, but also sets + # the right configuration in .npmrc that reads NPM token + # from NPM_AUTH_TOKEN environment variable. + # It actually creates a .npmrc in a temporary folder + # and sets the NPM_CONFIG_USERCONFIG environment variable. + registry-url: https://registry.npmjs.org + cache: 'pnpm' + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile + + - name: Build + shell: bash + run: pnpm --filter qwik build + + - run: pnpm dlx pkg-pr-new publish --pnpm ./packages/qwik + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN is provided automatically in any repository diff --git a/packages/qwik/src/actions/formAction$.ts b/packages/qwik/src/actions/formAction$.ts index 443b24ab..6a612a6c 100644 --- a/packages/qwik/src/actions/formAction$.ts +++ b/packages/qwik/src/actions/formAction$.ts @@ -1,6 +1,7 @@ import { $, implicit$FirstArg, noSerialize, type QRL } from '@builder.io/qwik'; import { globalActionQrl, + routeActionQrl, type Action, type RequestEventAction, } from '@builder.io/qwik-city'; @@ -26,7 +27,7 @@ import type { */ export type FormActionResult< TFieldValues extends FieldValues, - TResponseData extends ResponseData + TResponseData extends ResponseData, > = FormResponse & { errors?: Maybe>; }; @@ -36,7 +37,7 @@ export type FormActionResult< */ export type FormActionFunction< TFieldValues extends FieldValues, - TResponseData extends ResponseData + TResponseData extends ResponseData, > = ( values: TFieldValues, event: RequestEventAction @@ -52,11 +53,11 @@ export type FormActionArg2 = }); /** - * See {@link formAction$} + * See {@link routeFormAction$} */ -export function formActionQrl< +export function routeFormActionQrl< TFieldValues extends FieldValues, - TResponseData extends ResponseData = undefined + TResponseData extends ResponseData = undefined, >( action: QRL>, arg2: FormActionArg2 @@ -65,100 +66,79 @@ export function formActionQrl< PartialValues, true > { - return globalActionQrl( + return routeActionQrl( $( async ( jsonData: unknown, event: RequestEventAction ): Promise> => { - // Destructure validate function and form data info - const { validate, ...formDataInfo } = - typeof arg2 === 'object' ? arg2 : { validate: arg2 }; - - // Get content type of request - const type = event.request.headers - .get('content-type') - ?.split(/[;,]/, 1)[0]; - - // Get form values from form or JSON data - const values: PartialValues = - type === 'application/x-www-form-urlencoded' || - type === 'multipart/form-data' - ? decode( - event.sharedMap.get('@actionFormData'), - formDataInfo, - ({ output }) => - output instanceof Blob ? noSerialize(output) : output - ) - : (jsonData as PartialValues); - - // Validate values and get errors if necessary - const errors = validate ? await validate(values) : {}; - - // Create form action store object - let formActionStore: FormActionStore = { - values, - errors, - response: {}, - }; + return formActionLogic( + jsonData, + event, + action, + arg2 + ); + } + ), + { + id: action.getHash(), + } + ); +} - // Try to run submit action if form has no errors - if (!Object.keys(errors).length) { - try { - const result = await action(values as TFieldValues, event); - - // Add result to form action store if necessary - if (result && typeof result === 'object') { - formActionStore = { - values, - errors: result.errors || {}, - response: { - status: result.status, - message: result.message, - data: result.data, - }, - }; - } - - // If an abort message was thrown (e.g. a redirect), forward it - } catch (error) { - if ( - error instanceof AbortMessage || - (isDev && - (error?.constructor?.name === 'AbortMessage' || - error?.constructor?.name === 'RedirectMessage')) - ) { - throw error; - - // Otherwise log error and set error response - } else { - console.error(error); - - // If it is an expected error, use its error info - if (error instanceof FormError) { - formActionStore = { - values, - errors: error.errors, - response: { - status: 'error', - message: error.message, - }, - }; - - // Otherwise return a generic message to avoid leaking - // sensetive information - } else { - formActionStore.response = { - status: 'error', - message: 'An unknown error has occurred.', - }; - } - } - } - } +/** + + * Creates an action for progressively enhanced forms that handles validation + * and submission on the server. + * + * If you want to use it inside of a component, make sure you re-export it + * from either the `index.tsx` or `layout.tsx` that contains that component. + * see https://qwik.dev/docs/re-exporting-loaders/ for ho to do it. + * + * @param action The server action function. + * @param arg2 Validation and/or form data info. + * + * @returns Form action constructor. + + */ +export const routeFormAction$: < + TFieldValues extends FieldValues, + TResponseData extends ResponseData = undefined, +>( + actionQrl: FormActionFunction, + arg2: FormActionArg2 +) => Action< + FormActionStore, + PartialValues, + true +> = implicit$FirstArg(routeFormActionQrl); - // Return form action store object - return formActionStore; +/** + * See {@link globalFormAction$} + */ +export function globalFormActionQrl< + TFieldValues extends FieldValues, + TResponseData extends ResponseData = undefined, +>( + action: QRL>, + arg2: FormActionArg2 +): Action< + FormActionStore, + PartialValues, + true +> { + return globalActionQrl( + $( + async ( + jsonData: unknown, + event: RequestEventAction + ): Promise> => { + return formActionLogic( + jsonData, + event, + action, + arg2 + ); } ), { @@ -168,6 +148,9 @@ export function formActionQrl< } /** + + * If you need a form action that is route protected (by auth), use `routeFormAction$` instead. + * * Creates an action for progressively enhanced forms that handles validation * and submission on the server. * @@ -175,10 +158,39 @@ export function formActionQrl< * @param arg2 Validation and/or form data info. * * @returns Form action constructor. + + */ +export const globalFormAction$: < + TFieldValues extends FieldValues, + TResponseData extends ResponseData = undefined, +>( + actionQrl: FormActionFunction, + arg2: FormActionArg2 +) => Action< + FormActionStore, + PartialValues, + true +> = implicit$FirstArg(globalFormActionQrl); + +/** + * See {@link formAction$} + */ +export const formActionQrl = globalFormActionQrl; + +/** + * Creates an action for progressively enhanced forms that handles validation + * and submission on the server. + * + * @param action The server action function. + * @param arg2 Validation and/or form data info. + * + * @returns Form action constructor. + + * @deprecated Use `routeFormAction$` (recommended) or `globalFormAction$` instead. */ export const formAction$: < TFieldValues extends FieldValues, - TResponseData extends ResponseData = undefined + TResponseData extends ResponseData = undefined, >( actionQrl: FormActionFunction, arg2: FormActionArg2 @@ -187,3 +199,104 @@ export const formAction$: < PartialValues, true > = implicit$FirstArg(formActionQrl); + +/** + * @internal + */ + +export async function formActionLogic< + TFieldValues extends FieldValues, + TResponseData extends ResponseData = undefined, +>( + jsonData: unknown, + event: RequestEventAction, + action: QRL>, + arg2: FormActionArg2 +) { + // Destructure validate function and form data info + const { validate, ...formDataInfo } = + typeof arg2 === 'object' ? arg2 : { validate: arg2 }; + + // Get content type of request + const type = event.request.headers.get('content-type')?.split(/[;,]/, 1)[0]; + + // Get form values from form or JSON data + const values: PartialValues = + type === 'application/x-www-form-urlencoded' || + type === 'multipart/form-data' + ? decode( + event.sharedMap.get('@actionFormData'), + formDataInfo, + ({ output }) => + output instanceof Blob ? noSerialize(output) : output + ) + : (jsonData as PartialValues); + + // Validate values and get errors if necessary + const errors = validate ? await validate(values) : {}; + + // Create form action store object + let formActionStore: FormActionStore = { + values, + errors, + response: {}, + }; + + // Try to run submit action if form has no errors + if (!Object.keys(errors).length) { + try { + const result = await action(values as TFieldValues, event); + + // Add result to form action store if necessary + if (result && typeof result === 'object') { + formActionStore = { + values, + errors: result.errors || {}, + response: { + status: result.status, + message: result.message, + data: result.data, + }, + }; + } + + // If an abort message was thrown (e.g. a redirect), forward it + } catch (error) { + if ( + error instanceof AbortMessage || + (isDev && + (error?.constructor?.name === 'AbortMessage' || + error?.constructor?.name === 'RedirectMessage')) + ) { + throw error; + + // Otherwise log error and set error response + } else { + console.error(error); + + // If it is an expected error, use its error info + if (error instanceof FormError) { + formActionStore = { + values, + errors: error.errors, + response: { + status: 'error', + message: error.message, + }, + }; + + // Otherwise return a generic message to avoid leaking + // sensetive information + } else { + formActionStore.response = { + status: 'error', + message: 'An unknown error has occurred.', + }; + } + } + } + } + + // Return form action store object + return formActionStore; +} From 95e3da7571256742f0929b0c8185c8320db2069f Mon Sep 17 00:00:00 2001 From: Shai Reznik Date: Tue, 26 Nov 2024 18:32:31 +0200 Subject: [PATCH 2/3] renamed unclear argument --- packages/qwik/src/actions/formAction$.ts | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/qwik/src/actions/formAction$.ts b/packages/qwik/src/actions/formAction$.ts index 6a612a6c..28fd37fa 100644 --- a/packages/qwik/src/actions/formAction$.ts +++ b/packages/qwik/src/actions/formAction$.ts @@ -45,8 +45,15 @@ export type FormActionFunction< /** * Value type of the second form action argument. + * @deprecated Use `FormDataOrValidation` instead. */ export type FormActionArg2 = + FormDataOrValidation; + +/** + * Value type of the second form action argument. + */ +export type FormDataOrValidation = | QRL> | (FormDataInfo & { validate: QRL>; @@ -60,7 +67,7 @@ export function routeFormActionQrl< TResponseData extends ResponseData = undefined, >( action: QRL>, - arg2: FormActionArg2 + dataOrValidation: FormDataOrValidation ): Action< FormActionStore, PartialValues, @@ -76,7 +83,7 @@ export function routeFormActionQrl< jsonData, event, action, - arg2 + dataOrValidation ); } ), @@ -96,7 +103,7 @@ export function routeFormActionQrl< * see https://qwik.dev/docs/re-exporting-loaders/ for ho to do it. * * @param action The server action function. - * @param arg2 Validation and/or form data info. + * @param dataOrValidation Validation and/or form data info. * * @returns Form action constructor. @@ -106,7 +113,7 @@ export const routeFormAction$: < TResponseData extends ResponseData = undefined, >( actionQrl: FormActionFunction, - arg2: FormActionArg2 + dataOrValidation: FormDataOrValidation ) => Action< FormActionStore, PartialValues, @@ -121,7 +128,7 @@ export function globalFormActionQrl< TResponseData extends ResponseData = undefined, >( action: QRL>, - arg2: FormActionArg2 + dataOrValidation: FormDataOrValidation ): Action< FormActionStore, PartialValues, @@ -137,7 +144,7 @@ export function globalFormActionQrl< jsonData, event, action, - arg2 + dataOrValidation ); } ), @@ -155,7 +162,7 @@ export function globalFormActionQrl< * and submission on the server. * * @param action The server action function. - * @param arg2 Validation and/or form data info. + * @param dataOrValidation Validation and/or form data info. * * @returns Form action constructor. @@ -165,7 +172,7 @@ export const globalFormAction$: < TResponseData extends ResponseData = undefined, >( actionQrl: FormActionFunction, - arg2: FormActionArg2 + dataOrValidation: FormDataOrValidation ) => Action< FormActionStore, PartialValues, @@ -193,7 +200,7 @@ export const formAction$: < TResponseData extends ResponseData = undefined, >( actionQrl: FormActionFunction, - arg2: FormActionArg2 + dataOrValidation: FormActionArg2 ) => Action< FormActionStore, PartialValues, @@ -203,7 +210,6 @@ export const formAction$: < /** * @internal */ - export async function formActionLogic< TFieldValues extends FieldValues, TResponseData extends ResponseData = undefined, @@ -211,11 +217,13 @@ export async function formActionLogic< jsonData: unknown, event: RequestEventAction, action: QRL>, - arg2: FormActionArg2 + dataOrValidation: FormDataOrValidation ) { // Destructure validate function and form data info const { validate, ...formDataInfo } = - typeof arg2 === 'object' ? arg2 : { validate: arg2 }; + typeof dataOrValidation === 'object' + ? dataOrValidation + : { validate: dataOrValidation }; // Get content type of request const type = event.request.headers.get('content-type')?.split(/[;,]/, 1)[0]; From e75c62e0a6b91277d74c6b7575d40c8cb3a17489 Mon Sep 17 00:00:00 2001 From: Shai Reznik Date: Tue, 26 Nov 2024 18:34:09 +0200 Subject: [PATCH 3/3] might help with tree shaking --- packages/qwik/src/actions/formAction$.ts | 88 ++++++++++++------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/qwik/src/actions/formAction$.ts b/packages/qwik/src/actions/formAction$.ts index 28fd37fa..3c9c5712 100644 --- a/packages/qwik/src/actions/formAction$.ts +++ b/packages/qwik/src/actions/formAction$.ts @@ -6,7 +6,7 @@ import { type RequestEventAction, } from '@builder.io/qwik-city'; import { AbortMessage } from '@builder.io/qwik-city/middleware/request-handler'; -import { isDev } from '@builder.io/qwik/build'; +import { isDev, isServer } from '@builder.io/qwik/build'; import { decode } from 'decode-formdata'; import { FormError } from '../exceptions'; import type { @@ -250,56 +250,58 @@ export async function formActionLogic< response: {}, }; - // Try to run submit action if form has no errors - if (!Object.keys(errors).length) { - try { - const result = await action(values as TFieldValues, event); + if (isServer) { + // Try to run submit action if form has no errors + if (!Object.keys(errors).length) { + try { + const result = await action(values as TFieldValues, event); - // Add result to form action store if necessary - if (result && typeof result === 'object') { - formActionStore = { - values, - errors: result.errors || {}, - response: { - status: result.status, - message: result.message, - data: result.data, - }, - }; - } - - // If an abort message was thrown (e.g. a redirect), forward it - } catch (error) { - if ( - error instanceof AbortMessage || - (isDev && - (error?.constructor?.name === 'AbortMessage' || - error?.constructor?.name === 'RedirectMessage')) - ) { - throw error; - - // Otherwise log error and set error response - } else { - console.error(error); - - // If it is an expected error, use its error info - if (error instanceof FormError) { + // Add result to form action store if necessary + if (result && typeof result === 'object') { formActionStore = { values, - errors: error.errors, + errors: result.errors || {}, response: { - status: 'error', - message: error.message, + status: result.status, + message: result.message, + data: result.data, }, }; + } - // Otherwise return a generic message to avoid leaking - // sensetive information + // If an abort message was thrown (e.g. a redirect), forward it + } catch (error) { + if ( + error instanceof AbortMessage || + (isDev && + (error?.constructor?.name === 'AbortMessage' || + error?.constructor?.name === 'RedirectMessage')) + ) { + throw error; + + // Otherwise log error and set error response } else { - formActionStore.response = { - status: 'error', - message: 'An unknown error has occurred.', - }; + console.error(error); + + // If it is an expected error, use its error info + if (error instanceof FormError) { + formActionStore = { + values, + errors: error.errors, + response: { + status: 'error', + message: error.message, + }, + }; + + // Otherwise return a generic message to avoid leaking + // sensetive information + } else { + formActionStore.response = { + status: 'error', + message: 'An unknown error has occurred.', + }; + } } } }