From 815c272cc47054dd10ebc95499ab7efa91217c92 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sun, 19 Jul 2026 00:31:47 +0900 Subject: [PATCH 1/6] Support the innerContent-based Custom HTML block (WP 7.1) Since Gutenberg PR #79115 (WP 7.1) the core/html block stores its markup in `innerContent` instead of the `content` attribute, so the existing Edit component could no longer update the block content. Add an innerContent-based Edit component that reads content via `getBlockContent` and writes it via `updateBlock`/`replaceInnerBlocks`, and switch between it and the legacy Edit by inspecting the `content` attribute definition (`role: 'local'` vs `source: 'raw'`). Co-Authored-By: Claude --- src/block-editor/edit-legacy.tsx | 204 +++++++++++++++++++++++++++++++ src/block-editor/edit.tsx | 77 +++++++++++- src/block-editor/index.ts | 10 +- 3 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 src/block-editor/edit-legacy.tsx diff --git a/src/block-editor/edit-legacy.tsx b/src/block-editor/edit-legacy.tsx new file mode 100644 index 00000000..61f9dde0 --- /dev/null +++ b/src/block-editor/edit-legacy.tsx @@ -0,0 +1,204 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useMemo, useState, useRef } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { + BlockControls, + transformStyles, + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { + ResizableBox, + ToolbarButton, + Disabled, + SandBox, + ToolbarGroup, + Modal, + Notice, +} from '@wordpress/components'; +import { Stack } from '@wordpress/ui'; +import { fullscreen } from '@wordpress/icons'; +import type { BlockEditProps } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './style.scss'; +import MonacoEditor, { type MonacoError } from '../components/monaco-editor'; + +type HTMLEditProps = BlockEditProps< { + content: string; + height: number; +} > & { + // Injected by the block editor but not included in BlockEditProps. + toggleSelection: ( isSelectionEnabled: boolean ) => void; +}; + +const MIN_HEIGHT = 100; +const MAX_HEIGHT = 1000; + +const DEFAULT_STYLES = ` + html,body,:root { + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; + min-height: auto !important; + } +`; + +export default function HTMLEdit( { + attributes, + isSelected, + setAttributes, + toggleSelection, +}: HTMLEditProps ) { + const { content, height } = attributes; + const { editorSettings, editorOptions } = window.chbeObj; + + const [ isPreview, setIsPreview ] = useState< boolean >(); + const [ isModalEditorOpen, setIsModalEditorOpen ] = useState< boolean >(); + + const [ errorMessage, setErrorMessage ] = useState< string >(); + + const ref = useRef< HTMLDivElement >( null ); + + const settingStyles = useSelect( + ( select ) => select( blockEditorStore ).getSettings().styles, + [] + ); + + const styles = useMemo( + () => [ + DEFAULT_STYLES, + ...transformStyles( + ( settingStyles ?? [] ).filter( ( style: { css?: string } ) => style.css ) + ), + ], + [ settingStyles ] + ); + + const onResizeStart = () => { + toggleSelection( false ); + }; + + const onResizeStop = ( + _event: unknown, + _direction: unknown, + _elt: unknown, + delta: { height: number } + ) => { + const newHeight = Math.min( + Math.max( parseInt( `${ height + delta.height }`, 10 ), MIN_HEIGHT ), + MAX_HEIGHT + ); + setAttributes( { height: newHeight } ); + }; + + const onChange = ( value: string ) => { + setAttributes( { content: value } ); + }; + + const onError = ( error: MonacoError ) => { + if ( ( error.type === 'timeout' || error.type === 'scripterror' ) && error.msg ) { + setErrorMessage( error.msg ); + } + }; + + function switchToPreview() { + setIsPreview( true ); + } + + function switchToHTML() { + setIsPreview( false ); + } + + return ( +
+ + { ! isPreview && ( + + setIsModalEditorOpen( true ) } + /> + + ) } + + + { __( 'HTML', 'custom-html-block-extension' ) } + + + { __( 'Preview', 'custom-html-block-extension' ) } + + + + + { ( isDisabled ) => + isPreview || isDisabled ? ( + <> + + { ! isSelected &&
} + + ) : ( + + { errorMessage && { errorMessage } } + + + + + ) + } +
+ { isModalEditorOpen && ( + setIsModalEditorOpen( false ) } + > + + + ) } +
+ ); +} diff --git a/src/block-editor/edit.tsx b/src/block-editor/edit.tsx index 61f9dde0..c3b8d8ec 100644 --- a/src/block-editor/edit.tsx +++ b/src/block-editor/edit.tsx @@ -2,14 +2,15 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useMemo, useState, useRef } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; +import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; +import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; import { BlockControls, transformStyles, useBlockProps, store as blockEditorStore, } from '@wordpress/block-editor'; +import { parse, serialize, getBlockContent } from '@wordpress/blocks'; import { ResizableBox, ToolbarButton, @@ -21,7 +22,7 @@ import { } from '@wordpress/components'; import { Stack } from '@wordpress/ui'; import { fullscreen } from '@wordpress/icons'; -import type { BlockEditProps } from '@wordpress/blocks'; +import type { Block, BlockEditProps } from '@wordpress/blocks'; /** * Internal dependencies @@ -37,6 +38,13 @@ type HTMLEditProps = BlockEditProps< { toggleSelection: ( isSelectionEnabled: boolean ) => void; }; +// The parsed block carries `innerContent` (static HTML fragments interleaved +// with `null` markers for inner block positions), which the installed +// `@wordpress/blocks` types don't yet expose on `Block`. +type BlockWithInnerContent = Block & { + innerContent?: Array< string | null >; +}; + const MIN_HEIGHT = 100; const MAX_HEIGHT = 1000; @@ -50,12 +58,13 @@ const DEFAULT_STYLES = ` `; export default function HTMLEdit( { + clientId, attributes, isSelected, setAttributes, toggleSelection, }: HTMLEditProps ) { - const { content, height } = attributes; + const { height } = attributes; const { editorSettings, editorOptions } = window.chbeObj; const [ isPreview, setIsPreview ] = useState< boolean >(); @@ -65,6 +74,17 @@ export default function HTMLEdit( { const ref = useRef< HTMLDivElement >( null ); + const registry = useRegistry(); + const { updateBlock, replaceInnerBlocks } = useDispatch( blockEditorStore ); + + const content = useSelect( + ( select ) => { + const block = select( blockEditorStore ).getBlock( clientId ); + return block ? getBlockContent( block ) : ''; + }, + [ clientId ] + ); + const settingStyles = useSelect( ( select ) => select( blockEditorStore ).getSettings().styles, [] @@ -97,10 +117,55 @@ export default function HTMLEdit( { setAttributes( { height: newHeight } ); }; - const onChange = ( value: string ) => { - setAttributes( { content: value } ); + // Re-parse the edited content: static HTML becomes the block's + // `innerContent` fragments and `` delimited segments become + // inner blocks mounted at their positions within the static markup. + const onChange = ( nextContent: string ) => { + if ( nextContent === content ) { + return; + } + + const [ parsedBlock ] = parse( + `\n${ nextContent }\n` + ) as BlockWithInnerContent[]; + const nextInnerBlocks = parsedBlock?.innerBlocks ?? []; + const prevInnerBlocks = registry.select( blockEditorStore ).getBlocks( clientId ); + // Keep the existing inner blocks, and thereby their client IDs and + // selection, when their markup is unchanged — e.g. when only the + // surrounding static HTML was edited. + const innerBlocksUnchanged = + prevInnerBlocks.length === nextInnerBlocks.length && + prevInnerBlocks.every( + ( block: Block, index: number ) => + serialize( block ) === serialize( nextInnerBlocks[ index ] ) + ); + + registry.batch( () => { + updateBlock( clientId, { + innerContent: parsedBlock?.innerContent ?? ( nextContent ? [ nextContent ] : [] ), + } as Partial< Block > ); + if ( ! innerBlocksUnchanged ) { + replaceInnerBlocks( clientId, nextInnerBlocks, false ); + } + } ); }; + // Migrate the deprecated `content` attribute. The block's markup now lives + // in its inner content, but a block created via `createBlock( 'core/html', + // { content } )` still arrives with a `content` attribute. As soon as it + // loads, move that markup into the block's inner content and clear the + // attribute. + useEffect( () => { + if ( ! attributes.content ) { + return; + } + + updateBlock( clientId, { + attributes: { content: undefined }, + innerContent: [ attributes.content ], + } as Partial< Block > ); + }, [ attributes.content ] ); + const onError = ( error: MonacoError ) => { if ( ( error.type === 'timeout' || error.type === 'scripterror' ) && error.msg ) { setErrorMessage( error.msg ); diff --git a/src/block-editor/index.ts b/src/block-editor/index.ts index a6ac2928..9fd2e525 100644 --- a/src/block-editor/index.ts +++ b/src/block-editor/index.ts @@ -9,12 +9,20 @@ import type { BlockConfiguration } from '@wordpress/blocks'; */ import icon from '../components/block-icon'; import edit from './edit'; +import editLegacy from './edit-legacy'; const customHtmlBlockExtension = ( settings: BlockConfiguration ): BlockConfiguration => { if ( 'core/html' !== settings.name ) { return settings; } + // Since WordPress 7.1 the Custom HTML block stores its markup in + // `innerContent` instead of the `content` attribute, so its `content` + // attribute definition drops `source: 'raw'` in favor of `role: 'local'`. + // Use that to pick the matching Edit implementation. + // TODO: Remove `editLegacy` once the minimum supported version is 7.1. + const hasInnerContent = settings.attributes?.content?.role === 'local'; + const newSettings: BlockConfiguration = { ...settings, icon, @@ -25,7 +33,7 @@ const customHtmlBlockExtension = ( settings: BlockConfiguration ): BlockConfigur default: 300, }, }, - edit: edit as BlockConfiguration[ 'edit' ], + edit: ( hasInnerContent ? edit : editLegacy ) as BlockConfiguration[ 'edit' ], }; return newSettings; }; From 98efd50585eaef854b154e8d9f36e498333f49f8 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sun, 19 Jul 2026 01:05:37 +0900 Subject: [PATCH 2/6] Render embedded inner blocks as editable and add a preview placeholder On WordPress 7.1 a Custom HTML block can contain `` inner blocks. Show them as editable blocks in preview mode via the block-editor `InnerContent` private API, so they can be edited in place instead of being rendered as inert serialized HTML. Plain HTML (no inner blocks) still uses the sandboxed render to keep the live CSS/JS preview. Also show a placeholder in preview mode when the block has no content, instead of an empty sandbox iframe. Co-Authored-By: Claude --- src/block-editor/edit.tsx | 64 +++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/src/block-editor/edit.tsx b/src/block-editor/edit.tsx index c3b8d8ec..8430ad22 100644 --- a/src/block-editor/edit.tsx +++ b/src/block-editor/edit.tsx @@ -4,8 +4,10 @@ import { __ } from '@wordpress/i18n'; import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; +import * as blockEditor from '@wordpress/block-editor'; import { BlockControls, + BlockIcon, transformStyles, useBlockProps, store as blockEditorStore, @@ -19,9 +21,11 @@ import { ToolbarGroup, Modal, Notice, + Placeholder, } from '@wordpress/components'; import { Stack } from '@wordpress/ui'; -import { fullscreen } from '@wordpress/icons'; +import { code, fullscreen } from '@wordpress/icons'; +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; import type { Block, BlockEditProps } from '@wordpress/blocks'; /** @@ -30,6 +34,20 @@ import type { Block, BlockEditProps } from '@wordpress/blocks'; import './style.scss'; import MonacoEditor, { type MonacoError } from '../components/monaco-editor'; +// Opt in to the `@wordpress/block-editor` private APIs to access `InnerContent`, +// which renders a Custom HTML block's embedded inner blocks as editable blocks. +// This is a private API with no backward-compatibility guarantee: the consent +// string and `InnerContent` itself may change or be removed in any WordPress +// release. +const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/block-editor' +); + +const { InnerContent } = unlock( + ( blockEditor as unknown as { privateApis: unknown } ).privateApis +) as { InnerContent: React.ComponentType< { clientId: string } > }; + type HTMLEditProps = BlockEditProps< { content: string; height: number; @@ -77,10 +95,13 @@ export default function HTMLEdit( { const registry = useRegistry(); const { updateBlock, replaceInnerBlocks } = useDispatch( blockEditorStore ); - const content = useSelect( + const { content, hasInnerBlocks } = useSelect( ( select ) => { const block = select( blockEditorStore ).getBlock( clientId ); - return block ? getBlockContent( block ) : ''; + return { + content: block ? getBlockContent( block ) : '', + hasInnerBlocks: ( block?.innerBlocks?.length ?? 0 ) > 0, + }; }, [ clientId ] ); @@ -164,6 +185,7 @@ export default function HTMLEdit( { attributes: { content: undefined }, innerContent: [ attributes.content ], } as Partial< Block > ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ attributes.content ] ); const onError = ( error: MonacoError ) => { @@ -202,13 +224,31 @@ export default function HTMLEdit( { - { ( isDisabled ) => - isPreview || isDisabled ? ( - <> - - { ! isSelected &&
} - - ) : ( + { ( isDisabled ) => { + if ( isPreview || isDisabled ) { + if ( ! content?.trim() ) { + return ( + } + label={ __( 'Custom HTML', 'custom-html-block-extension' ) } + instructions={ __( + 'Add custom HTML code and preview how it looks.', + 'custom-html-block-extension' + ) } + /> + ); + } + if ( hasInnerBlocks && isPreview && ! isDisabled ) { + return ; + } + return ( + <> + + { ! isSelected &&
} + + ); + } + return ( { errorMessage && { errorMessage } } - ) - } + ); + } }
{ isModalEditorOpen && ( Date: Sun, 19 Jul 2026 01:18:22 +0900 Subject: [PATCH 3/6] Detect preview via isPreviewMode instead of Disabled context Core stopped wrapping block previews in the `Disabled` component and removed its use from the Custom HTML block in #73108, standardizing on the `isPreviewMode` editor setting. `Disabled.Consumer` only covers the auto-height block preview, so post/template previews and zoom-out would still mount the heavy Monaco editor. Read `getSettings().isPreviewMode` instead to cover every preview path and drop the `Disabled` dependency. Also list `clientId` and `updateBlock` in the migration effect's deps (both are stable) instead of suppressing the exhaustive-deps lint. Co-Authored-By: Claude --- src/block-editor/edit.tsx | 139 +++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/src/block-editor/edit.tsx b/src/block-editor/edit.tsx index 8430ad22..34dafeb0 100644 --- a/src/block-editor/edit.tsx +++ b/src/block-editor/edit.tsx @@ -16,7 +16,6 @@ import { parse, serialize, getBlockContent } from '@wordpress/blocks'; import { ResizableBox, ToolbarButton, - Disabled, SandBox, ToolbarGroup, Modal, @@ -106,10 +105,13 @@ export default function HTMLEdit( { [ clientId ] ); - const settingStyles = useSelect( - ( select ) => select( blockEditorStore ).getSettings().styles, - [] - ); + const { settingStyles, isPreviewMode } = useSelect( ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + return { + settingStyles: settings.styles, + isPreviewMode: ( settings as { isPreviewMode?: boolean } ).isPreviewMode, + }; + }, [] ); const styles = useMemo( () => [ @@ -185,8 +187,7 @@ export default function HTMLEdit( { attributes: { content: undefined }, innerContent: [ attributes.content ], } as Partial< Block > ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ attributes.content ] ); + }, [ clientId, updateBlock, attributes.content ] ); const onError = ( error: MonacoError ) => { if ( ( error.type === 'timeout' || error.type === 'scripterror' ) && error.msg ) { @@ -202,6 +203,33 @@ export default function HTMLEdit( { setIsPreview( false ); } + function renderPreview() { + if ( ! content?.trim() ) { + return ( + } + label={ __( 'Custom HTML', 'custom-html-block-extension' ) } + instructions={ __( + 'Add custom HTML code and preview how it looks.', + 'custom-html-block-extension' + ) } + /> + ); + } + // Render editable inner blocks in place, but only in the user-toggled + // preview — not in the read-only preview mode, where nothing should be + // interactive. + if ( hasInnerBlocks && isPreview && ! isPreviewMode ) { + return ; + } + return ( + <> + + { ! isSelected &&
} + + ); + } + return (
@@ -223,67 +251,42 @@ export default function HTMLEdit( { - - { ( isDisabled ) => { - if ( isPreview || isDisabled ) { - if ( ! content?.trim() ) { - return ( - } - label={ __( 'Custom HTML', 'custom-html-block-extension' ) } - instructions={ __( - 'Add custom HTML code and preview how it looks.', - 'custom-html-block-extension' - ) } - /> - ); - } - if ( hasInnerBlocks && isPreview && ! isDisabled ) { - return ; - } - return ( - <> - - { ! isSelected &&
} - - ); - } - return ( - - { errorMessage && { errorMessage } } - - - - - ); - } } -
+ { isPreview || isPreviewMode ? ( + renderPreview() + ) : ( + + { errorMessage && { errorMessage } } + + + + + ) } { isModalEditorOpen && ( Date: Sun, 19 Jul 2026 01:41:43 +0900 Subject: [PATCH 4/6] Render preview via InnerContent only, dropping the sandboxed preview Align with core, which renders the Custom HTML block through `InnerContent` alone. The sandboxed live CSS/JS preview and its `hasInnerBlocks` branch are no longer needed, so drop `SandBox`, the editor-style injection, and the preview overlay. Preview mode now always renders `InnerContent`, matching the editor canvas. Also merge the two `useSelect` calls into one and inline the private-API unlock. Co-Authored-By: Claude --- src/block-editor/edit.tsx | 97 ++++++++++----------------------------- 1 file changed, 23 insertions(+), 74 deletions(-) diff --git a/src/block-editor/edit.tsx b/src/block-editor/edit.tsx index 34dafeb0..d36d30ee 100644 --- a/src/block-editor/edit.tsx +++ b/src/block-editor/edit.tsx @@ -2,13 +2,12 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; +import { useEffect, useState, useRef } from '@wordpress/element'; import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; import * as blockEditor from '@wordpress/block-editor'; import { BlockControls, BlockIcon, - transformStyles, useBlockProps, store as blockEditorStore, } from '@wordpress/block-editor'; @@ -16,7 +15,6 @@ import { parse, serialize, getBlockContent } from '@wordpress/blocks'; import { ResizableBox, ToolbarButton, - SandBox, ToolbarGroup, Modal, Notice, @@ -33,19 +31,12 @@ import type { Block, BlockEditProps } from '@wordpress/blocks'; import './style.scss'; import MonacoEditor, { type MonacoError } from '../components/monaco-editor'; -// Opt in to the `@wordpress/block-editor` private APIs to access `InnerContent`, -// which renders a Custom HTML block's embedded inner blocks as editable blocks. -// This is a private API with no backward-compatibility guarantee: the consent -// string and `InnerContent` itself may change or be removed in any WordPress -// release. -const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( +const { InnerContent } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', '@wordpress/block-editor' -); - -const { InnerContent } = unlock( - ( blockEditor as unknown as { privateApis: unknown } ).privateApis -) as { InnerContent: React.ComponentType< { clientId: string } > }; +).unlock( ( blockEditor as unknown as { privateApis: unknown } ).privateApis ) as { + InnerContent: React.ComponentType< { clientId: string } >; +}; type HTMLEditProps = BlockEditProps< { content: string; @@ -55,9 +46,6 @@ type HTMLEditProps = BlockEditProps< { toggleSelection: ( isSelectionEnabled: boolean ) => void; }; -// The parsed block carries `innerContent` (static HTML fragments interleaved -// with `null` markers for inner block positions), which the installed -// `@wordpress/blocks` types don't yet expose on `Block`. type BlockWithInnerContent = Block & { innerContent?: Array< string | null >; }; @@ -65,15 +53,6 @@ type BlockWithInnerContent = Block & { const MIN_HEIGHT = 100; const MAX_HEIGHT = 1000; -const DEFAULT_STYLES = ` - html,body,:root { - margin: 0 !important; - padding: 0 !important; - overflow: visible !important; - min-height: auto !important; - } -`; - export default function HTMLEdit( { clientId, attributes, @@ -94,34 +73,19 @@ export default function HTMLEdit( { const registry = useRegistry(); const { updateBlock, replaceInnerBlocks } = useDispatch( blockEditorStore ); - const { content, hasInnerBlocks } = useSelect( + const { content, isPreviewMode } = useSelect( ( select ) => { - const block = select( blockEditorStore ).getBlock( clientId ); + const { getBlock, getSettings } = select( blockEditorStore ); + const block = getBlock( clientId ); return { content: block ? getBlockContent( block ) : '', - hasInnerBlocks: ( block?.innerBlocks?.length ?? 0 ) > 0, + isPreviewMode: ( getSettings() as { isPreviewMode?: boolean } ).isPreviewMode, }; }, [ clientId ] ); - const { settingStyles, isPreviewMode } = useSelect( ( select ) => { - const settings = select( blockEditorStore ).getSettings(); - return { - settingStyles: settings.styles, - isPreviewMode: ( settings as { isPreviewMode?: boolean } ).isPreviewMode, - }; - }, [] ); - - const styles = useMemo( - () => [ - DEFAULT_STYLES, - ...transformStyles( - ( settingStyles ?? [] ).filter( ( style: { css?: string } ) => style.css ) - ), - ], - [ settingStyles ] - ); + const hasContent = !! content.trim(); const onResizeStart = () => { toggleSelection( false ); @@ -203,33 +167,6 @@ export default function HTMLEdit( { setIsPreview( false ); } - function renderPreview() { - if ( ! content?.trim() ) { - return ( - } - label={ __( 'Custom HTML', 'custom-html-block-extension' ) } - instructions={ __( - 'Add custom HTML code and preview how it looks.', - 'custom-html-block-extension' - ) } - /> - ); - } - // Render editable inner blocks in place, but only in the user-toggled - // preview — not in the read-only preview mode, where nothing should be - // interactive. - if ( hasInnerBlocks && isPreview && ! isPreviewMode ) { - return ; - } - return ( - <> - - { ! isSelected &&
} - - ); - } - return (
@@ -252,7 +189,19 @@ export default function HTMLEdit( { { isPreview || isPreviewMode ? ( - renderPreview() + <> + { ! hasContent && ( + } + label={ __( 'Custom HTML', 'custom-html-block-extension' ) } + instructions={ __( + 'Add custom HTML code and preview how it looks.', + 'custom-html-block-extension' + ) } + /> + ) } + { hasContent && } + ) : ( { errorMessage && { errorMessage } } From 03f4a7324a7b85b87755dc70417cb11fbb91dd3a Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sun, 19 Jul 2026 15:17:46 +0900 Subject: [PATCH 5/6] Type block-editor privateApis to drop the unknown cast The namespace import lacked a privateApis type, forcing an `as unknown as` double cast to reach it. Augment the package types with a `privateApis` declaration so `unlock()` can consume it directly. Co-Authored-By: Claude --- src/block-editor/edit.tsx | 2 +- src/global.d.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/block-editor/edit.tsx b/src/block-editor/edit.tsx index d36d30ee..05110404 100644 --- a/src/block-editor/edit.tsx +++ b/src/block-editor/edit.tsx @@ -34,7 +34,7 @@ import MonacoEditor, { type MonacoError } from '../components/monaco-editor'; const { InnerContent } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', '@wordpress/block-editor' -).unlock( ( blockEditor as unknown as { privateApis: unknown } ).privateApis ) as { +).unlock( blockEditor.privateApis ) as { InnerContent: React.ComponentType< { clientId: string } >; }; diff --git a/src/global.d.ts b/src/global.d.ts index 9460c8e2..c89de721 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -51,4 +51,9 @@ declare module 'react' { } } +// `privateApis` is exported at runtime but missing from the package types. +declare module '@wordpress/block-editor' { + export const privateApis: unknown; +} + export {}; From 481111cf0ada398cd4e1ff6d699dafdc759d5c0e Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sun, 19 Jul 2026 15:29:16 +0900 Subject: [PATCH 6/6] Declare @wordpress/private-apis as a direct dependency edit.tsx imports __dangerousOptInToUnstableAPIsOnlyForCoreModules directly from @wordpress/private-apis, but the package was only resolved transitively via @wordpress/block-editor. Declare it explicitly to avoid relying on a phantom dependency that could break under stricter package managers or if block-editor drops it. Co-Authored-By: Claude --- package-lock.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2cee4b81..904b2684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@wordpress/i18n": "^6.21.1", "@wordpress/keycodes": "^4.34.0", "@wordpress/notices": "^5.48.1", + "@wordpress/private-apis": "^1.49.0", "@wordpress/scripts": "^32.4.1", "clsx": "2.1.1", "husky": "^9.1.7", diff --git a/package.json b/package.json index 7ff0c1d9..9acbef22 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@wordpress/i18n": "^6.21.1", "@wordpress/keycodes": "^4.34.0", "@wordpress/notices": "^5.48.1", + "@wordpress/private-apis": "^1.49.0", "@wordpress/scripts": "^32.4.1", "clsx": "2.1.1", "husky": "^9.1.7",