Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/content-tab-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import DeleteSite from 'src/components/delete-site';
import { useGetWpVersion } from 'src/hooks/use-get-wp-version';
import { getIpcApi } from 'src/lib/get-ipc-api';
import EditSiteDetails from 'src/modules/site-settings/edit-site-details';
import { useAppDispatch } from 'src/stores';
import { useAppDispatch, useRootSelector } from 'src/stores';
import { betaFeaturesSelectors } from 'src/stores/beta-features-slice';
import {
certificateTrustApi,
useCheckCertificateTrustQuery,
Expand All @@ -33,6 +34,7 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
const dispatch = useAppDispatch();
const { __ } = useI18n();
const { data: isCertificateTrusted } = useCheckCertificateTrustQuery();
const betaFeatures = useRootSelector( betaFeaturesSelectors.selectBetaFeatures );
const username = 'admin';
// Empty strings account for legacy sites lacking a stored password.
const storedPassword = decodePassword( selectedSite.adminPassword ?? '' );
Expand Down Expand Up @@ -132,6 +134,11 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
<span className="line-clamp-1 break-all">{ selectedSite.phpVersion }</span>
</div>
</SettingsRow>
{ betaFeatures.xdebugSupport && (
<SettingsRow label={ __( 'Xdebug' ) }>
<span>{ selectedSite.enableXdebug ? __( 'Enabled' ) : __( 'Disabled' ) }</span>
</SettingsRow>
) }

<tr>
<th colSpan={ 2 } className="pb-4 ltr:text-left rtl:text-right">
Expand Down
78 changes: 70 additions & 8 deletions src/components/tests/content-tab-settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ function snapshotTestReducer( state: RootState | undefined, action: UnknownActio
} );
}

// Use the test reducer but preserve provider constants
// Use the test reducer but preserve provider constants and beta features
const newState = testReducer( state, action );

// If we have provider constants in the current state, preserve them
// If we have provider constants or beta features in the current state, preserve them
const preservedState = { ...newState };
if ( state?.providerConstants ) {
return {
...newState,
providerConstants: state.providerConstants,
};
preservedState.providerConstants = state.providerConstants;
}
if ( state?.betaFeatures ) {
preservedState.betaFeatures = state.betaFeatures;
}

return newState;
return preservedState;
}

const snapshotTestActions = {
Expand All @@ -52,6 +53,16 @@ let testStore = createTestStore( {
allowedPhpVersions: [ '8.0', '8.1', '8.2', '8.3' ],
minimumWordPressVersion: '6.2.6',
},
preloadedState: {
betaFeatures: {
features: {
studioSitesCli: false,
multiWorkerSupport: false,
xdebugSupport: true,
},
loading: false,
},
},
} );

// We need to create a new store each time to avoid reducer conflicts
Expand All @@ -63,6 +74,16 @@ function createCustomTestStore() {
allowedPhpVersions: [ '8.0', '8.1', '8.2', '8.3' ],
minimumWordPressVersion: '6.2.6',
},
preloadedState: {
betaFeatures: {
features: {
studioSitesCli: false,
multiWorkerSupport: false,
xdebugSupport: true,
},
loading: false,
},
},
} );
store.replaceReducer( snapshotTestReducer );
return store;
Expand Down Expand Up @@ -116,6 +137,7 @@ describe( 'ContentTabSettings', () => {
const openLocalPath = jest.fn();
const generateProposedSitePath = jest.fn();
const getAllCustomDomains = jest.fn().mockResolvedValue( [] );
const getXdebugEnabledSite = jest.fn().mockResolvedValue( null );
const mockSnapshot = {
localSiteId: selectedSite.id,
url: 'http://localhost:8881',
Expand All @@ -137,6 +159,7 @@ describe( 'ContentTabSettings', () => {
openLocalPath,
generateProposedSitePath,
getAllCustomDomains,
getXdebugEnabledSite,
isCATrusted: jest.fn( () => Promise.resolve( true ) ),
} );

Expand Down Expand Up @@ -164,7 +187,9 @@ describe( 'ContentTabSettings', () => {
screen.getByRole( 'button', { name: 'localhost:8881, Copy site url to clipboard' } )
).toHaveTextContent( 'localhost:8881' );
expect( screen.getByText( 'HTTPS' ) ).toBeVisible();
expect( screen.getByText( 'Disabled' ) ).toBeVisible();
expect( screen.getByText( 'Xdebug' ) ).toBeVisible();
// Both HTTPS and Xdebug show "Disabled"
expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 2 );
expect( screen.getByRole( 'button', { name: 'Copy local path to clipboard' } ) ).toBeVisible();
expect( screen.getByText( '7.7.7' ) ).toBeVisible();
expect(
Expand Down Expand Up @@ -253,6 +278,43 @@ describe( 'ContentTabSettings', () => {
} );
} );

describe( 'Xdebug beta feature', () => {
it( 'hides Xdebug row when beta feature is disabled', async () => {
// Create a store with xdebugSupport disabled
const storeWithoutXdebug = createTestStore( {
providerConstants: {
defaultPhpVersion: '8.3',
defaultWordPressVersion: 'latest',
allowedPhpVersions: [ '8.0', '8.1', '8.2', '8.3' ],
minimumWordPressVersion: '6.2.6',
},
preloadedState: {
betaFeatures: {
features: {
studioSitesCli: false,
multiWorkerSupport: false,
xdebugSupport: false,
},
loading: false,
},
},
} );

render(
<Provider store={ storeWithoutXdebug }>
<ContentTabSettings selectedSite={ selectedSite } />
</Provider>
);

await waitFor( () => {
expect( getAllCustomDomains ).toHaveBeenCalled();
} );

// Xdebug row should not be visible
expect( screen.queryByText( 'Xdebug' ) ).not.toBeInTheDocument();
} );
} );

describe( 'PHP version', () => {
it( 'changes PHP version when site is not running', async () => {
const user = userEvent.setup();
Expand Down
12 changes: 12 additions & 0 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ export async function getSiteDetails( _event: IpcMainInvokeEvent ): Promise< Sit
return mergeSiteDetailsWithRunningDetails( sites );
}

export async function getXdebugEnabledSite(
_event: IpcMainInvokeEvent
): Promise< SiteDetails | null > {
const userData = await loadUserData();
const { sites } = userData;
const xdebugSite = sites.find( ( site ) => site.enableXdebug );
if ( ! xdebugSite ) {
return null;
}
return mergeSiteDetailsWithRunningDetails( [ xdebugSite ] )[ 0 ] || null;
}

export async function importSite(
event: IpcMainInvokeEvent,
{ id, backupFile }: { id: string; backupFile: BackupArchiveInfo }
Expand Down
2 changes: 2 additions & 0 deletions src/ipc-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface StoppedSiteDetails {
};
isAddingSite?: boolean;
autoStart?: boolean;
enableXdebug?: boolean;
}

interface StartedSiteDetails extends StoppedSiteDetails {
Expand Down Expand Up @@ -84,6 +85,7 @@ interface FeatureFlags {
interface BetaFeatures {
studioSitesCli: boolean;
multiWorkerSupport: boolean;
xdebugSupport: boolean;
}

interface AppGlobals extends FeatureFlags {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/beta-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const BETA_FEATURES_DEFINITION: Record< keyof BetaFeatures, BetaFeatureDefinitio
default: false,
description: 'Enable multi-worker PHP processing for faster performance',
},
xdebugSupport: {
label: 'Xdebug Support',
key: 'xdebugSupport',
default: false,
description: 'Enable PHP debugging with Xdebug (one site at a time)',
},
} as const;

export const BETA_FEATURES: Record< keyof BetaFeatures, BetaFeatureDefinition > =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface PlaygroundCliOptions {
wordpressInstallMode: WordPressInstallMode;
blueprint?: Blueprint;
enableMultiWorker?: boolean;
enableXdebug?: boolean;
}

export const PLAYGROUND_CLI_PROVIDER_NAME = 'playground-cli';
Expand Down Expand Up @@ -61,18 +62,23 @@ export class PlaygroundCliProvider implements WordPressProvider {
siteLanguage?: string;
wpCliPharPath?: string;
blueprint?: Blueprint;
enableXdebug?: boolean;
} ): Promise< WordPressServerInstance > {
const port = options.port;
const phpVersion = options.phpVersion || '8.3';
const hasWordPress = isWordPressDirectory( options.path );

// Get beta features to check if multi-worker support is enabled
// Get beta features to check if multi-worker and xdebug support are enabled
const betaFeatures = await getBetaFeatures();

if ( betaFeatures.multiWorkerSupport ) {
console.log( '[PlaygroundCliProvider] Multi-worker support is enabled via beta features' );
}

if ( betaFeatures.xdebugSupport && options.enableXdebug ) {
console.log( '[PlaygroundCliProvider] Xdebug support is enabled for this site' );
}

const playgroundOptions: PlaygroundCliOptions = {
port,
phpVersion,
Expand All @@ -83,6 +89,7 @@ export class PlaygroundCliProvider implements WordPressProvider {
: 'download-and-install',
blueprint: options.blueprint,
enableMultiWorker: betaFeatures.multiWorkerSupport,
enableXdebug: betaFeatures.xdebugSupport && options.enableXdebug,
};

const serverOptions: WordPressServerOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ async function startServer(
args.experimentalMultiWorker = workerCount;
}

// Enable Xdebug support if requested
if ( options.enableXdebug ) {
console.log( __( 'Enabling Xdebug support' ) );
args.xdebug = true;

@sejas sejas Dec 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add experimentalUnsafeIdeIntegration argument or is not necessary? I tried it to add it, but it didn't help during my testing.

args.experimentalUnsafeIdeIntegration = [ 'vscode', 'phpstorm' ];`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because it's experimental and unsafe :D I tested it, and besides being broken, it was only updating IDE configs. I think it's better if Studio enables user to use Xdebug, instead of modifying their IDE configs.

}

if ( options.phpVersion ) {
args.php = options.phpVersion as SupportedPHPVersion;
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/wordpress-provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ServerOptions {
absoluteUrl?: string;
siteLanguage?: string;
blueprint?: Blueprint[ 'blueprint' ];
enableXdebug?: boolean;
}

export interface WordPressServerOptions {
Expand Down
Loading