diff --git a/packages/block-library/tsconfig.json b/packages/block-library/tsconfig.json index 621c77f60fe00f..13581ab393bb6e 100644 --- a/packages/block-library/tsconfig.json +++ b/packages/block-library/tsconfig.json @@ -34,6 +34,7 @@ { "path": "../keyboard-shortcuts" }, { "path": "../primitives" }, { "path": "../rich-text" }, + { "path": "../server-side-render" }, { "path": "../shortcode" }, { "path": "../ui" }, { "path": "../upload-media" }, diff --git a/packages/editor/tsconfig.json b/packages/editor/tsconfig.json index 38865b20a306e4..4cd6bce8787db7 100644 --- a/packages/editor/tsconfig.json +++ b/packages/editor/tsconfig.json @@ -38,6 +38,7 @@ { "path": "../preferences" }, { "path": "../private-apis" }, { "path": "../rich-text" }, + { "path": "../server-side-render" }, { "path": "../ui" }, { "path": "../url" }, { "path": "../upload-media" }, diff --git a/packages/server-side-render/README.md b/packages/server-side-render/README.md index 4df7faaee452fe..78658c64184eb7 100644 --- a/packages/server-side-render/README.md +++ b/packages/server-side-render/README.md @@ -63,20 +63,12 @@ function Example() { _Parameters_ -- _props_ `Object`: Component props. -- _props.block_ `string`: The identifier of the block to be serverside rendered. -- _props.attributes_ `Object`: The block attributes to be sent to the server for rendering. -- _props.className_ `[string]`: Additional classes to apply to the wrapper element. -- _props.httpMethod_ `[string]`: The HTTP method to use ('GET' or 'POST'). Default is 'GET' -- _props.urlQueryArgs_ `[Object]`: Additional query arguments to append to the request URL. -- _props.skipBlockSupportAttributes_ `[boolean]`: Whether to remove block support attributes before sending. -- _props.EmptyResponsePlaceholder_ `[Function]`: Component rendered when the API response is empty. -- _props.ErrorResponsePlaceholder_ `[Function]`: Component rendered when the API response is an error. -- _props.LoadingResponsePlaceholder_ `[Function]`: Component rendered while the API request is loading. +- _props_ `ServerSideRenderWithPostIdProps`: Component props. +- _props.urlQueryArgs_ `ServerSideRenderWithPostIdProps[ 'urlQueryArgs' ]`: Additional query arguments to append to the request URL. _Returns_ -- `React.JSX.Element`: The rendered server-side content. +- The rendered server-side content. ### useServerSideRender @@ -112,12 +104,7 @@ function MyServerSideRender( { attributes, block } ) { _Parameters_ -- _args_ `Object`: The hook configuration object. -- _args.attributes_ `Object`: The block attributes to be sent to the server for rendering. -- _args.block_ `string`: The identifier of the block to be serverside rendered. Example: 'core/archives'. -- _args.skipBlockSupportAttributes_ `[boolean]`: Whether to remove block support attributes before sending. -- _args.httpMethod_ `[string]`: The HTTP method to use ('GET' or 'POST'). Default is 'GET'. -- _args.urlQueryArgs_ `[Object]`: Additional query arguments to append to the request URL. +- _args_ `UseServerSideRenderArgs`: The hook configuration object. _Returns_ diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index e019dec1887d9d..b6d54d8f5a165e 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -34,6 +34,7 @@ "module": "build-module/index.mjs", "exports": { ".": { + "types": "./build-types/index.d.ts", "import": "./build-module/index.mjs", "require": "./build/index.cjs" }, @@ -42,6 +43,7 @@ "react-native": "src/index", "wpScript": true, "wpScriptDefaultExport": true, + "types": "build-types/index.d.ts", "dependencies": { "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blocks": "file:../blocks", diff --git a/packages/server-side-render/src/hook.js b/packages/server-side-render/src/hook.ts similarity index 62% rename from packages/server-side-render/src/hook.js rename to packages/server-side-render/src/hook.ts index 1d19a6e7c170b1..24116794a41a10 100644 --- a/packages/server-side-render/src/hook.js +++ b/packages/server-side-render/src/hook.ts @@ -5,9 +5,14 @@ import { debounce } from '@wordpress/compose'; import { useEffect, useState, useRef } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; + import { __experimentalSanitizeBlockAttributes } from '@wordpress/blocks'; -export function rendererPath( block, attributes = null, urlQueryArgs = {} ) { +export function rendererPath( + block: string, + attributes: Record< string, unknown > | null = null, + urlQueryArgs: Record< string, unknown > = {} +): string { return addQueryArgs( `/wp/v2/block-renderer/${ block }`, { context: 'edit', ...( null !== attributes ? { attributes } : {} ), @@ -15,7 +20,11 @@ export function rendererPath( block, attributes = null, urlQueryArgs = {} ) { } ); } -export function removeBlockSupportAttributes( attributes ) { +export function removeBlockSupportAttributes( + attributes: Record< string, unknown > & { + style?: Record< string, unknown >; + } +): Record< string, unknown > { const { backgroundColor, borderColor, @@ -44,11 +53,32 @@ export function removeBlockSupportAttributes( attributes ) { } /** - * @typedef {Object} ServerSideRenderResponse - * @property {string} status - The current request status: 'idle', 'loading', 'success', or 'error'. - * @property {string} [content] - The rendered block content (available when status is 'success'). - * @property {string} [error] - The error message (available when status is 'error'). + * Server-side render response object. + */ +export interface ServerSideRenderResponse { + /** The current request status: 'idle', 'loading', 'success', or 'error'. */ + status: 'idle' | 'loading' | 'success' | 'error'; + /** The rendered block content (available when status is 'success'). */ + content?: string; + /** The error message (available when status is 'error'). */ + error?: string; +} + +/** + * Configuration object for the useServerSideRender hook. */ +export interface UseServerSideRenderArgs { + /** The block attributes to be sent to the server for rendering. */ + attributes: Record< string, unknown >; + /** The identifier of the block to be serverside rendered. Example: 'core/archives'. */ + block: string; + /** Whether to remove block support attributes before sending. */ + skipBlockSupportAttributes?: boolean; + /** The HTTP method to use ('GET' or 'POST'). Default is 'GET'. */ + httpMethod?: 'GET' | 'POST'; + /** Additional query arguments to append to the request URL. */ + urlQueryArgs?: Record< string, unknown >; +} /** * A hook for server-side rendering a preview of dynamic blocks to display in the editor. @@ -82,18 +112,17 @@ export function removeBlockSupportAttributes( attributes ) { * } * ``` * - * @param {Object} args The hook configuration object. - * @param {Object} args.attributes The block attributes to be sent to the server for rendering. - * @param {string} args.block The identifier of the block to be serverside rendered. Example: 'core/archives'. - * @param {boolean} [args.skipBlockSupportAttributes=false] Whether to remove block support attributes before sending. - * @param {string} [args.httpMethod='GET'] The HTTP method to use ('GET' or 'POST'). Default is 'GET'. - * @param {Object} [args.urlQueryArgs] Additional query arguments to append to the request URL. + * @param args The hook configuration object. * - * @return {ServerSideRenderResponse} The server-side render response object. + * @return The server-side render response object. */ -export function useServerSideRender( args ) { - const [ response, setResponse ] = useState( { status: 'idle' } ); - const shouldDebounceRef = useRef( false ); +export function useServerSideRender( + args: UseServerSideRenderArgs +): ServerSideRenderResponse { + const [ response, setResponse ] = useState< ServerSideRenderResponse >( { + status: 'idle', + } ); + const shouldDebounceRef = useRef< boolean >( false ); const { attributes, @@ -103,11 +132,11 @@ export function useServerSideRender( args ) { urlQueryArgs, } = args; - let sanitizedAttributes = + let sanitizedAttributes: Record< string, unknown > | null = attributes && __experimentalSanitizeBlockAttributes( block, attributes ); - if ( skipBlockSupportAttributes ) { + if ( skipBlockSupportAttributes && sanitizedAttributes ) { sanitizedAttributes = removeBlockSupportAttributes( sanitizedAttributes ); } @@ -128,7 +157,7 @@ export function useServerSideRender( args ) { { setResponse( { status: 'loading' } ); - apiFetch( { + apiFetch< { rendered: string } >( { path, method: isPostRequest ? 'POST' : 'GET', body, @@ -145,15 +174,21 @@ export function useServerSideRender( args ) { content: res ? res.rendered : '', } ); } ) - .catch( ( error ) => { + .catch( ( error: unknown ) => { // The request was aborted, do not update the response. - if ( error.name === 'AbortError' ) { + if ( + error instanceof Error && + error.name === 'AbortError' + ) { return; } setResponse( { status: 'error', - error: error.message, + error: + error instanceof Error + ? error.message + : String( error ), } ); } ) .finally( () => { diff --git a/packages/server-side-render/src/index.js b/packages/server-side-render/src/index.ts similarity index 76% rename from packages/server-side-render/src/index.js rename to packages/server-side-render/src/index.ts index 28bc8abfec43bd..245d52ceba27b3 100644 --- a/packages/server-side-render/src/index.js +++ b/packages/server-side-render/src/index.ts @@ -14,7 +14,12 @@ import { useServerSideRender } from './hook'; * import ServerSideRender from '@wordpress/server-side-render'; * ``` */ -const ServerSideRenderCompat = ServerSideRenderWithPostId; +const ServerSideRenderCompat = + ServerSideRenderWithPostId as typeof ServerSideRenderWithPostId & { + ServerSideRender: typeof ServerSideRenderWithPostId; + useServerSideRender: typeof useServerSideRender; + }; + ServerSideRenderCompat.ServerSideRender = ServerSideRenderWithPostId; ServerSideRenderCompat.useServerSideRender = useServerSideRender; diff --git a/packages/server-side-render/src/server-side-render.js b/packages/server-side-render/src/server-side-render.tsx similarity index 71% rename from packages/server-side-render/src/server-side-render.js rename to packages/server-side-render/src/server-side-render.tsx index 8100574bc31136..676ed45f92fb10 100644 --- a/packages/server-side-render/src/server-side-render.js +++ b/packages/server-side-render/src/server-side-render.tsx @@ -5,8 +5,8 @@ import { RawHTML, useEffect, useState, - useRef, useMemo, + useRef, } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { Placeholder, Spinner } from '@wordpress/components'; @@ -16,10 +16,17 @@ import { useSelect } from '@wordpress/data'; * Internal dependencies */ import { useServerSideRender } from './hook'; +import type { + PlaceholderProps, + ErrorPlaceholderProps, + LoadingPlaceholderProps, + ServerSideRenderProps, + ServerSideRenderWithPostIdProps, +} from './types'; const EMPTY_OBJECT = {}; -function DefaultEmptyResponsePlaceholder( { className } ) { +function DefaultEmptyResponsePlaceholder( { className }: PlaceholderProps ) { return ( { __( 'Block rendered as empty.' ) } @@ -27,16 +34,21 @@ function DefaultEmptyResponsePlaceholder( { className } ) { ); } -function DefaultErrorResponsePlaceholder( { message, className } ) { +function DefaultErrorResponsePlaceholder( { + message, + className, +}: ErrorPlaceholderProps ) { const errorMessage = sprintf( // translators: %s: error message describing the problem __( 'Error loading block: %s' ), - message + message || __( 'Unknown error' ) ); return { errorMessage }; } -function DefaultLoadingResponsePlaceholder( { children } ) { +function DefaultLoadingResponsePlaceholder( { + children, +}: LoadingPlaceholderProps ) { const [ showLoader, setShowLoader ] = useState( false ); useEffect( () => { @@ -69,7 +81,7 @@ function DefaultLoadingResponsePlaceholder( { children } ) { ); } -export function ServerSideRender( props ) { +export function ServerSideRender( props: ServerSideRenderProps ) { const prevContentRef = useRef( '' ); const { className, @@ -91,9 +103,13 @@ export function ServerSideRender( props ) { if ( status === 'loading' ) { return ( + { /* eslint-disable-next-line react-hooks/refs */ } { !! prevContentRef.current && ( - { prevContentRef.current } + { + // eslint-disable-next-line react-hooks/refs + prevContentRef.current + } ) } @@ -108,7 +124,7 @@ export function ServerSideRender( props ) { return ; } - return { content }; + return { content || '' }; } /** @@ -135,28 +151,21 @@ export function ServerSideRender( props ) { * } * ``` * - * @param {Object} props Component props. - * @param {string} props.block The identifier of the block to be serverside rendered. - * @param {Object} props.attributes The block attributes to be sent to the server for rendering. - * @param {string} [props.className] Additional classes to apply to the wrapper element. - * @param {string} [props.httpMethod='GET'] The HTTP method to use ('GET' or 'POST'). Default is 'GET' - * @param {Object} [props.urlQueryArgs] Additional query arguments to append to the request URL. - * @param {boolean} [props.skipBlockSupportAttributes=false] Whether to remove block support attributes before sending. - * @param {Function} [props.EmptyResponsePlaceholder] Component rendered when the API response is empty. - * @param {Function} [props.ErrorResponsePlaceholder] Component rendered when the API response is an error. - * @param {Function} [props.LoadingResponsePlaceholder] Component rendered while the API request is loading. - * - * @return {React.JSX.Element} The rendered server-side content. + * @param props Component props. + * @param props.urlQueryArgs Additional query arguments to append to the request URL. + * @return The rendered server-side content. */ export function ServerSideRenderWithPostId( { urlQueryArgs = EMPTY_OBJECT, ...props -} ) { +}: ServerSideRenderWithPostIdProps ) { const currentPostId = useSelect( ( select ) => { // FIXME: @wordpress/server-side-render should not depend on @wordpress/editor. // It is used by blocks that can be loaded into a *non-post* block editor. // eslint-disable-next-line @wordpress/data-no-store-string-literals - const postId = select( 'core/editor' )?.getCurrentPostId(); + const editorStore = select( 'core/editor' ); + // @ts-expect-error - editorStore is not typed in @wordpress/data + const postId = editorStore?.getCurrentPostId?.(); // For templates and template parts we use a custom ID format. // Since they aren't real posts, we don't want to use their ID diff --git a/packages/server-side-render/src/test/index.js b/packages/server-side-render/src/test/index.ts similarity index 100% rename from packages/server-side-render/src/test/index.js rename to packages/server-side-render/src/test/index.ts diff --git a/packages/server-side-render/src/types.ts b/packages/server-side-render/src/types.ts new file mode 100644 index 00000000000000..97a24b9b2ff571 --- /dev/null +++ b/packages/server-side-render/src/types.ts @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import type React from 'react'; + +/** + * Internal dependencies + */ +import type { UseServerSideRenderArgs } from './hook'; + +export interface PlaceholderProps { + /** Additional classes to apply to the wrapper element. */ + className?: string; +} + +export interface ErrorPlaceholderProps extends PlaceholderProps { + /** Error message describing the problem. */ + message?: string; +} + +export interface LoadingPlaceholderProps { + children?: React.ReactNode; +} + +export interface ServerSideRenderProps extends UseServerSideRenderArgs { + /** Additional classes to apply to the wrapper element. */ + className?: string; + /** Component rendered when the API response is empty. */ + EmptyResponsePlaceholder?: React.ComponentType< PlaceholderProps >; + /** Component rendered when the API response is an error. */ + ErrorResponsePlaceholder?: React.ComponentType< ErrorPlaceholderProps >; + /** Component rendered while the API request is loading. */ + LoadingResponsePlaceholder?: React.ComponentType< LoadingPlaceholderProps >; +} + +export interface ServerSideRenderWithPostIdProps + extends Omit< ServerSideRenderProps, 'urlQueryArgs' > { + /** Additional query arguments to append to the request URL. */ + urlQueryArgs?: Record< string, unknown >; +} diff --git a/packages/server-side-render/tsconfig.json b/packages/server-side-render/tsconfig.json new file mode 100644 index 00000000000000..02ca31143328e6 --- /dev/null +++ b/packages/server-side-render/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": [ "gutenberg-env", "gutenberg-test-env", "jest" ] + }, + "references": [ + { "path": "../api-fetch" }, + { "path": "../blocks" }, + { "path": "../components" }, + { "path": "../compose" }, + { "path": "../data" }, + { "path": "../deprecated" }, + { "path": "../element" }, + { "path": "../i18n" }, + { "path": "../url" } + ] +} diff --git a/tsconfig.json b/tsconfig.json index fbceaebce0bd6e..82cf29f174ab89 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,7 @@ { "path": "packages/rich-text" }, { "path": "packages/route" }, { "path": "packages/router" }, + { "path": "packages/server-side-render" }, { "path": "packages/shortcode" }, { "path": "packages/style-engine" }, { "path": "packages/style-runtime" },