diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 56b2f70a..4b4e166a 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -58,6 +58,7 @@ class GutenbergView : WebView { private var openMediaLibraryListener: OpenMediaLibraryListener? = null private var editorDidBecomeAvailableListener: EditorAvailableListener? = null private var logJsExceptionListener: LogJsExceptionListener? = null + private var autocompleterTriggeredListener: AutocompleterTriggeredListener? = null var textEditorEnabled: Boolean = false set(value) { @@ -88,6 +89,10 @@ class GutenbergView : WebView { logJsExceptionListener = listener } + fun setAutocompleterTriggeredListener(listener: AutocompleterTriggeredListener) { + autocompleterTriggeredListener = listener + } + fun setOnFileChooserRequestedListener(listener: (Intent, Int) -> Unit) { onFileChooserRequested = listener } @@ -403,6 +408,10 @@ class GutenbergView : WebView { fun onLogJsException(exception: GutenbergJsException) } + interface AutocompleterTriggeredListener { + fun onAutocompleterTriggered(type: String) + } + fun getTitleAndContent(originalContent: CharSequence, callback: TitleAndContentCallback, completeComposition: Boolean = false) { if (!isEditorLoaded) { Log.e("GutenbergView", "You can't change the editor content until it has loaded") @@ -445,6 +454,17 @@ class GutenbergView : WebView { } } + fun appendTextAtCursor(text: String) { + if (!isEditorLoaded) { + Log.e("GutenbergView", "You can't append text until the editor has loaded") + return + } + val encodedText = encodeForEditor(text) + handler.post { + this.evaluateJavascript("editor.appendTextAtCursor(decodeURIComponent('$encodedText'));", null) + } + } + @JavascriptInterface fun onEditorLoaded() { Log.i("GutenbergView", "EditorLoaded received in native code") @@ -547,6 +567,13 @@ class GutenbergView : WebView { Log.i("GutenbergView", "BlockPickerShouldShow") } + @JavascriptInterface + fun onAutocompleterTriggered(type: String) { + handler.post { + autocompleterTriggeredListener?.onAutocompleterTriggered(type) + } + } + fun resetFilePathCallback() { filePathCallback = null } @@ -562,6 +589,7 @@ class GutenbergView : WebView { editorDidBecomeAvailable = null filePathCallback = null onFileChooserRequested = null + autocompleterTriggeredListener = null handler.removeCallbacksAndMessages(null) this.destroy() } diff --git a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift index 9f111647..6fc7de3f 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift @@ -37,6 +37,8 @@ struct EditorJSMessage { case showBlockPicker /// User requested the Media Library. case openMediaLibrary + /// The user triggered an autocompleter. + case onAutocompleterTriggered } struct DidUpdateBlocksBody: Decodable { @@ -51,4 +53,8 @@ struct EditorJSMessage { struct DidUpdateFeaturedImageBody: Decodable { let mediaID: Int } + + struct AutocompleterTriggeredBody: Decodable { + let type: String + } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index dcfea9a8..3a4cab08 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -297,6 +297,14 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro evaluate("editor.setMediaUploadAttachment(\(media));") } + /// Appends text at the current cursor position in the editor. + /// + /// - parameter text: The text to append at the cursor position. + public func appendTextAtCursor(_ text: String) { + let escapedText = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? text + evaluate("editor.appendTextAtCursor(decodeURIComponent('\(escapedText)'));") + } + // MARK: - GutenbergEditorControllerDelegate fileprivate func controller(_ controller: GutenbergEditorController, didReceiveMessage message: EditorJSMessage) { @@ -326,6 +334,9 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro case .openMediaLibrary: let config = try message.decode(OpenMediaLibraryAction.self) openMediaLibrary(config) + case .onAutocompleterTriggered: + let body = try message.decode(EditorJSMessage.AutocompleterTriggeredBody.self) + delegate?.editor(self, didTriggerAutocompleter: body.type) } } catch { fatalError("failed to decode message: \(error)") diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift index d8a87c84..89b30704 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift @@ -34,6 +34,11 @@ public protocol EditorViewControllerDelegate: AnyObject { func editor(_ viewController: EditorViewController, didLogException error: GutenbergJSException) func editor(_ viewController: EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) + + /// Notifies the client that an autocompleter was triggered. + /// + /// - parameter type: The type of autocompleter that was triggered (e.g., "plus-symbol", "at-symbol"). + func editor(_ viewController: EditorViewController, didTriggerAutocompleter type: String) } public struct EditorState { diff --git a/src/components/editor/index.jsx b/src/components/editor/index.jsx index 4108593e..6a97d841 100644 --- a/src/components/editor/index.jsx +++ b/src/components/editor/index.jsx @@ -19,6 +19,8 @@ import { useMediaUpload } from './use-media-upload'; import TextEditor from '../text-editor'; import { useSyncFeaturedImage } from './use-sync-featured-image'; import { useDevModeNotice } from './use-dev-mode-notice'; +import { useAtAutocompleter } from './use-at-autocompleter'; +import { usePlusAutocompleter } from './use-plus-autocompleter'; /** * @typedef {import('../utils/bridge').Post} Post @@ -43,6 +45,8 @@ export default function Editor( { post, children, hideTitle } ) { useEditorSetup( post ); useMediaUpload(); useDevModeNotice(); + useAtAutocompleter(); + usePlusAutocompleter(); const { isReady, mode, isRichEditingEnabled, currentPost } = useSelect( ( select ) => { diff --git a/src/components/editor/use-at-autocompleter.js b/src/components/editor/use-at-autocompleter.js new file mode 100644 index 00000000..0df5402f --- /dev/null +++ b/src/components/editor/use-at-autocompleter.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { addFilter, removeFilter } from '@wordpress/hooks'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { onAutocompleterTriggered } from '../../utils/bridge'; + +/** + * Adds a filter for the Autocomplete completers to show an alert when @ is typed. + * + * @return {void} + */ +export function useAtAutocompleter() { + useEffect( () => { + // Avoid conflicts with core's default autocompleter + removeFilter( + 'editor.Autocomplete.completers', + 'editor/autocompleters/set-default-completers' + ); + + addFilter( + 'editor.Autocomplete.completers', + 'GutenbergKit/at-symbol-alert', + addAtSymbolCompleter + ); + + return () => { + removeFilter( + 'editor.Autocomplete.completers', + 'GutenbergKit/at-symbol-alert' + ); + }; + }, [] ); +} + +/** + * Adds the @ symbol autocompleter to the completers array. + * + * @param {Array} completers Existing completers. + * @return {Array} Updated completers array. + */ +function addAtSymbolCompleter( completers = [] ) { + const atSymbolCompleter = { + name: 'at-symbol', + triggerPrefix: '@', + options: ( filterValue ) => { + // Only trigger when cursor is directly after @ (no characters typed yet) + if ( filterValue === '' ) { + onAutocompleterTriggered( 'at-symbol' ); + } + // Return empty array since we're not providing actual completion options + return []; + }, + allowContext: ( before, after ) => { + const beforeEmptyOrWhitespace = /^$|\s$/.test( before ); + const afterEmptyOrWhitespace = /^$|^\s/.test( after ); + return beforeEmptyOrWhitespace && afterEmptyOrWhitespace; + }, + isDebounced: true, + }; + + return [ ...completers, atSymbolCompleter ]; +} diff --git a/src/components/editor/use-host-bridge.js b/src/components/editor/use-host-bridge.js index ed292861..72ebd220 100644 --- a/src/components/editor/use-host-bridge.js +++ b/src/components/editor/use-host-bridge.js @@ -5,7 +5,14 @@ import { useEffect, useCallback, useRef } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; -import { parse, serialize } from '@wordpress/blocks'; +import { parse, serialize, getBlockType } from '@wordpress/blocks'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { insert, create, toHTMLString } from '@wordpress/rich-text'; + +/** + * Internal dependencies + */ +import { warn } from '../../utils/logger'; window.editor = window.editor || {}; @@ -14,6 +21,13 @@ export function useHostBridge( post, editorRef ) { const { undo, redo, switchEditorMode } = useDispatch( editorStore ); const { getEditedPostAttribute, getEditedPostContent } = useSelect( editorStore ); + const { updateBlock, selectionChange } = useDispatch( blockEditorStore ); + const { + getSelectedBlockClientId, + getBlock, + getSelectionStart, + getSelectionEnd, + } = useSelect( blockEditorStore ); const editContent = useCallback( ( edits ) => { @@ -79,6 +93,64 @@ export function useHostBridge( post, editorRef ) { switchEditorMode( mode ); }; + window.editor.appendTextAtCursor = ( text ) => { + const selectedBlockClientId = getSelectedBlockClientId(); + + if ( ! selectedBlockClientId ) { + warn( 'Unable to append text: no block selected' ); + return false; + } + + const block = getBlock( selectedBlockClientId ); + + if ( ! block ) { + warn( + 'Unable to append text: could not retrieve selected block' + ); + return false; + } + + const blockType = getBlockType( block.name ); + const hasContentAttribute = blockType?.attributes?.content; + + if ( ! hasContentAttribute ) { + warn( + `Unable to append text: block type ${ block.name } does not support text content` + ); + return false; + } + + const blockContent = block.attributes?.content || ''; + const currentValue = create( { html: blockContent } ); + const selectionStart = getSelectionStart(); + const selectionEnd = getSelectionEnd(); + const newValue = insert( + currentValue, + text, + selectionStart?.offset, + selectionEnd?.offset + ); + + updateBlock( selectedBlockClientId, { + attributes: { + ...block.attributes, + content: toHTMLString( { value: newValue } ), + }, + } ); + + const newCursorPosition = + selectionStart?.offset + text.length || newValue.text.length; + + selectionChange( { + clientId: selectionStart?.clientId || selectedBlockClientId, + attributeKey: selectionStart?.attributeKey || 'content', + startOffset: newCursorPosition, + endOffset: newCursorPosition, + } ); + + return true; + }; + return () => { delete window.editor.setContent; delete window.editor.setTitle; @@ -87,6 +159,7 @@ export function useHostBridge( post, editorRef ) { delete window.editor.undo; delete window.editor.redo; delete window.editor.switchEditorMode; + delete window.editor.appendTextAtCursor; }; }, [ editorRef, @@ -96,6 +169,12 @@ export function useHostBridge( post, editorRef ) { redo, switchEditorMode, undo, + getSelectedBlockClientId, + getBlock, + getSelectionStart, + getSelectionEnd, + updateBlock, + selectionChange, ] ); } diff --git a/src/components/editor/use-plus-autocompleter.js b/src/components/editor/use-plus-autocompleter.js new file mode 100644 index 00000000..f8aa0ebd --- /dev/null +++ b/src/components/editor/use-plus-autocompleter.js @@ -0,0 +1,61 @@ +/** + * WordPress dependencies + */ +import { addFilter, removeFilter } from '@wordpress/hooks'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { onAutocompleterTriggered } from '../../utils/bridge'; + +/** + * Adds a filter for the Autocomplete completers to show an alert when + is typed. + * + * @return {void} + */ +export function usePlusAutocompleter() { + useEffect( () => { + addFilter( + 'editor.Autocomplete.completers', + 'GutenbergKit/plus-symbol-alert', + addPlusSymbolCompleter + ); + + return () => { + removeFilter( + 'editor.Autocomplete.completers', + 'GutenbergKit/plus-symbol-alert' + ); + }; + }, [] ); +} + +/** + * Adds the + symbol autocompleter to the completers array. + * + * @param {Array} completers Existing completers. + * @return {Array} Updated completers array. + */ +function addPlusSymbolCompleter( completers = [] ) { + const plusSymbolCompleter = { + name: 'plus-symbol', + triggerPrefix: '+', + options: ( filterValue ) => { + // Only trigger when cursor is directly after + (no characters typed yet) + if ( filterValue === '' ) { + onAutocompleterTriggered( 'plus-symbol' ); + } + // Return empty array since we're not providing actual completion options + return []; + }, + allowContext: ( before, after ) => { + const beforeEmptyOrWhitespace = /^$|\s$/.test( before ); + const afterEmptyOrWhitespace = /^$|^\s/.test( after ); + return beforeEmptyOrWhitespace && afterEmptyOrWhitespace; + }, + isDebounced: true, + }; + + return [ ...completers, plusSymbolCompleter ]; +} diff --git a/src/remote.jsx b/src/remote.jsx index 93244991..ce169e46 100644 --- a/src/remote.jsx +++ b/src/remote.jsx @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import apiFetch from '@wordpress/api-fetch'; // Default styles that are needed for the editor. import '@wordpress/components/build-style/style.css'; import '@wordpress/block-editor/build-style/style.css'; @@ -17,16 +16,12 @@ import '@wordpress/editor/build-style/style.css'; * Internal dependencies */ import { awaitGBKitGlobal } from './utils/bridge'; -import { initializeApiFetch } from './utils/api-fetch'; import { loadEditorAssets } from './utils/remote-editor'; import { initializeVideoPressAjaxBridge } from './utils/videopress-bridge'; import { error, warn } from './utils/logger'; import { isDevMode } from './utils/dev-mode'; import './index.scss'; -window.wp = window.wp || {}; -window.wp.apiFetch = apiFetch; - const I18N_PACKAGES = [ 'i18n', 'hooks' ]; // Rely upon promises rather than async/await to avoid timeouts caused by @@ -43,12 +38,11 @@ awaitGBKitGlobal() .then( importL10n ) .then( configureLocale ) .then( loadRemainingAssets ) + .then( initializeApiFetch ) .then( initializeEditor ) .catch( handleError ); function initializeApiAndLoadI18n() { - initializeApiFetch(); - // Ensure the i18n packages are loaded, then set the locale before importing // the rest of the packages. return loadEditorAssets( { allowedPackages: I18N_PACKAGES } ); @@ -71,6 +65,15 @@ function loadRemainingAssets() { } ); } +function initializeApiFetch( assetsResult ) { + return import( './utils/api-fetch' ).then( + ( { initializeApiFetch: _initializeApiFetch } ) => { + _initializeApiFetch(); + return assetsResult; + } + ); +} + function initializeEditor( assetsResult ) { initializeVideoPressAjaxBridge(); diff --git a/src/utils/bridge.js b/src/utils/bridge.js index 22862d1f..e2d1b570 100644 --- a/src/utils/bridge.js +++ b/src/utils/bridge.js @@ -1,14 +1,10 @@ -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; - /** * Internal dependencies */ import parseException from './exception-parser'; import { debug } from './logger'; import { isDevMode } from './dev-mode'; +import { basicFetch } from './fetch'; /** * Generic function to dispatch messages to both Android and iOS bridges. @@ -121,6 +117,17 @@ export function openMediaLibrary( config ) { } } +/** + * Notifies the native host that an autocompleter was triggered. + * + * @param {string} type The type of autocompleter that was triggered (e.g. 'at-symbol', 'plus-symbol'). + * + * @return {void} + */ +export function onAutocompleterTriggered( type ) { + dispatchToBridge( 'onAutocompleterTriggered', { type } ); +} + /** * @typedef GBKitConfig * @@ -287,16 +294,15 @@ export function awaitGBKitGlobal( timeoutMs = 3000 ) { export async function fetchEditorAssets() { if ( window.webkit ) { return await window.webkit.messageHandlers.loadFetchedEditorAssets.postMessage( - { - asset: 'manifest', - } + { asset: 'manifest' } ); } - // Android implementation - uses same API call that will be intercepted - const { siteApiRoot, editorAssetsEndpoint } = getGBKit(); + + const { siteApiRoot, editorAssetsEndpoint, authHeader } = getGBKit(); const url = editorAssetsEndpoint || `${ siteApiRoot }wpcom/v2/editor-assets`; - return await apiFetch( { - url, + // Use our fetch utility, as we have not yet loaded the `wp.apiFetch` utility + return await basicFetch( url, { + headers: { Authorization: authHeader }, } ); } diff --git a/src/utils/fetch.js b/src/utils/fetch.js new file mode 100644 index 00000000..a5f22c4e --- /dev/null +++ b/src/utils/fetch.js @@ -0,0 +1,114 @@ +/** + * Basic fetch implementation based on `@wordpress/api-fetch`. + * + * @param {string} url The URL to fetch. + * @param {Object} options Fetch options. + * @return {Promise} Fetch promise. + */ +export function basicFetch( url, options = {} ) { + const responsePromise = window.fetch( url, options ); + + return responsePromise.then( + ( value ) => + Promise.resolve( value ) + .then( checkStatus ) + .catch( ( response ) => parseAndThrowError( response ) ) + .then( ( response ) => + parseResponseAndNormalizeError( response ) + ), + ( err ) => { + // Re-throw AbortError for the users to handle it themselves. + if ( err && err.name === 'AbortError' ) { + throw err; + } + + // Otherwise, there is most likely no network connection. + // Unfortunately the message might depend on the browser. + throw { + code: 'fetch_error', + message: 'You are probably offline.', + }; + } + ); +} + +/** + * Checks the status of a response, throwing an error if the status is not in the 200 range. + * + * @param {Response} response + * @return {Response} The response if the status is in the 200 range. + */ +function checkStatus( response ) { + if ( response.status >= 200 && response.status < 300 ) { + return response; + } + + throw response; +} + +/** + * Parses a response, throwing an error if parsing the response fails. + * + * @param {Response} response + * @return {Promise} Parsed response. + */ +function parseAndThrowError( response ) { + return parseJsonAndNormalizeError( response ).then( ( error ) => { + const unknownError = { + code: 'unknown_error', + message: 'An unknown error occurred.', + }; + + throw error || unknownError; + } ); +} + +/** + * Calls the `json` function on the Response, throwing an error if the response + * doesn't have a json function or if parsing the json itself fails. + * + * @param {Response} response + * @return {Promise} Parsed response. + */ +const parseJsonAndNormalizeError = ( response ) => { + const invalidJsonError = { + code: 'invalid_json', + message: 'The response is not a valid JSON response.', + }; + + if ( ! response || ! response.json ) { + throw invalidJsonError; + } + + return response.json().catch( () => { + throw invalidJsonError; + } ); +}; + +/** + * Parses the fetch response properly and normalize response errors. + * + * @param {Response} response + * + * @return {Promise} Parsed response. + */ +function parseResponseAndNormalizeError( response ) { + return Promise.resolve( parseResponse( response ) ).catch( ( res ) => + parseAndThrowError( res ) + ); +} + +/** + * Parses the fetch response. + * + * @param {Response} response + * + * @return {Promise | null | Response} Parsed response. + */ +function parseResponse( response ) { + if ( response.status === 204 ) { + return null; + } + + return response.json ? response.json() : Promise.reject( response ); +} diff --git a/src/utils/remote-editor.js b/src/utils/remote-editor.js index d70d0bab..7af092b1 100644 --- a/src/utils/remote-editor.js +++ b/src/utils/remote-editor.js @@ -107,16 +107,8 @@ async function loadAssets( html, { allowedPackages = [], disallowedPackages = [] } = {} ) { - /** - * Locally-sourced Gutenberg packages excluded from remote loading to avoid - * conflicts. - */ - const localGutenbergPackages = [ 'api-fetch', ...disallowedPackages ]; - const excludedScriptIDs = new RegExp( - localGutenbergPackages - .map( ( script ) => `wp-${ script }-js` ) - .join( '|' ) + disallowedPackages.map( ( script ) => `wp-${ script }-js` ).join( '|' ) ); const allowedScriptIDs = allowedPackages.length diff --git a/src/utils/videopress-bridge.js b/src/utils/videopress-bridge.js index 799c6524..7bed2ee2 100644 --- a/src/utils/videopress-bridge.js +++ b/src/utils/videopress-bridge.js @@ -2,7 +2,7 @@ * Internal dependencies */ import { getGBKit } from './bridge'; -import { warn, info, error } from './logger'; +import { warn, debug, error } from './logger'; /** * VideoPress AJAX to REST API bridge. @@ -62,7 +62,7 @@ export function initializeVideoPressAjaxBridge() { return deferred.promise(); }; - info( 'VideoPress AJAX bridge initialized' ); + debug( 'VideoPress AJAX bridge initialized' ); } /** @@ -89,7 +89,7 @@ function handleVideoPressUploadJWT() { }; delete processedResponse.upload_url; - info( + debug( 'VideoPress JWT obtained successfully', processedResponse ); diff --git a/vite.config.remote.js b/vite.config.remote.js index 7c2a98df..92b67524 100644 --- a/vite.config.remote.js +++ b/vite.config.remote.js @@ -18,7 +18,7 @@ export default defineConfig( { outDir: '../dist', rollupOptions: { input: resolve( __dirname, 'src/remote.html' ), - external: externalize, + external, }, target: 'esnext', }, @@ -33,43 +33,47 @@ export default defineConfig( { }, } ); -function externalize( id ) { - const externalDefinition = defaultRequestToExternal( id ); - return ( - !! externalDefinition && - ! id.match( /\.css(?:\?inline)?$/ ) && - ! [ 'apiFetch', 'i18n', 'url', 'hooks' ].includes( - externalDefinition[ externalDefinition.length - 1 ] - ) - ); +function external( id ) { + const hasExternal = defaultRequestToExternal( id ) !== undefined; + const isInlineCss = id.match( /\.css\?inline$/ ); + + return hasExternal && ! isInlineCss; } /** * Transform code by replacing WordPress imports with global definitions. + * E.g., `import { __ } from '@wordpress/i18n';` becomes `const { __ } = window.wp.i18n;` + * This replicates Gutenberg's behavior in a browser environment, which relies upon + * the `@wordpress/dependency-extraction-webpack-plugin` module. + * + * See: https://github.com/WordPress/gutenberg/tree/d2fce222ebbbef8dbc56eee1badcfe4ae0df04b0/packages/dependency-extraction-webpack-plugin * * @return {Object} The transformed code and map. */ function wordPressExternals() { return { name: 'wordpress-externals-plugin', - transform( code, id ) { + transform( code ) { const magicString = new MagicString( code ); let hasReplacements = false; // Match WordPress and React JSX runtime import statements const regex = - /import\s*(?:{([^}]+)}\s*from)?\s*['"](@wordpress\/([^'"]+)|react\/jsx-runtime)['"];/g; + /import\s*(?:(?:(\w+)|{([^}]+)})\s*from\s*)?['"](@wordpress\/[^'"]+|react\/jsx-runtime)['"];/g; let match; while ( ( match = regex.exec( code ) ) !== null ) { - const [ fullMatch, imports, module ] = match; + const [ fullMatch, defaultImport, namedImports, module ] = + match; + const imports = defaultImport || namedImports; + + if ( module.match( /\.css\?inline$/ ) ) { + continue; // Exclude inlined CSS files from externalization + } + const externalDefinition = defaultRequestToExternal( module ); - if ( - ! externalDefinition || - /@wordpress\/(api-fetch|url|hooks)/.test( id ) || - /@wordpress\/(api-fetch|url|hooks)/.test( module ) - ) { + if ( ! externalDefinition ) { continue; // Exclude the module from externalization } @@ -84,22 +88,31 @@ function wordPressExternals() { continue; } - const importList = imports.split( ',' ).map( ( i ) => { - const parts = i.trim().split( /\s+as\s+/ ); - if ( parts.length === 2 ) { - // Convert import "as" syntax to destructuring assignment - return `${ parts[ 0 ] }: ${ parts[ 1 ] }`; - } - return i.trim(); - } ); - const definitionArray = Array.isArray( externalDefinition ) ? externalDefinition : [ externalDefinition ]; - const replacement = `const { ${ importList.join( - ', ' - ) } } = window.${ definitionArray.join( '.' ) };`; + let replacement; + if ( defaultImport ) { + // Handle default import + replacement = `const ${ defaultImport } = window.${ definitionArray.join( + '.' + ) };`; + } else { + // Handle named imports + const importList = imports.split( ',' ).map( ( i ) => { + const parts = i.trim().split( /\s+as\s+/ ); + if ( parts.length === 2 ) { + // Convert import "as" syntax to destructuring assignment + return `${ parts[ 0 ] }: ${ parts[ 1 ] }`; + } + return i.trim(); + } ); + + replacement = `const { ${ importList.join( + ', ' + ) } } = window.${ definitionArray.join( '.' ) };`; + } magicString.overwrite( match.index, match.index + fullMatch.length,