diff --git a/apps/cli/commands/config/get.ts b/apps/cli/commands/config/get.ts index 8d46f0fac7..767fb0427d 100644 --- a/apps/cli/commands/config/get.ts +++ b/apps/cli/commands/config/get.ts @@ -2,6 +2,7 @@ import { getWordPressVersion } from '@studio/common/lib/get-wordpress-version'; import { decodePassword } from '@studio/common/lib/passwords'; import { getSiteFileAccess } from '@studio/common/lib/site-file-access'; import { getSiteRuntime, siteModeFromRuntime } from '@studio/common/lib/site-runtime'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { SiteCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions'; import { __, sprintf } from '@wordpress/i18n'; import CliTable3 from 'cli-table3'; @@ -43,6 +44,8 @@ function getConfigEntries( site: SiteData ): ConfigEntry[] { { key: 'admin-email', value: site.adminEmail }, { key: 'debug-log', value: site.enableDebugLog ?? false }, { key: 'debug-display', value: site.enableDebugDisplay ?? false }, + { key: 'script-debug', value: site.enableScriptDebug ?? false }, + { key: 'environment-type', value: getWpEnvironmentType( site ) }, ]; } diff --git a/apps/cli/commands/config/set.ts b/apps/cli/commands/config/set.ts index 18a42cd0e3..bc55e8e0cb 100644 --- a/apps/cli/commands/config/set.ts +++ b/apps/cli/commands/config/set.ts @@ -29,6 +29,12 @@ import { isValidWordPressVersion, isWordPressVersionAtLeast, } from '@studio/common/lib/wordpress-version-utils'; +import { + getWpEnvironmentType, + wpEnvironmentTypeSchema, + WP_ENVIRONMENT_TYPES, + type WpEnvironmentType, +} from '@studio/common/lib/wp-environment-type'; import { SiteCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions'; import { SupportedPHPVersions } from '@studio/common/types/php-versions'; import { __, sprintf } from '@wordpress/i18n'; @@ -70,6 +76,8 @@ export interface SetCommandOptions { adminEmail?: string; debugLog?: boolean; debugDisplay?: boolean; + scriptDebug?: boolean; + environmentType?: WpEnvironmentType; } export async function runCommand( sitePath: string, options: SetCommandOptions ): Promise< void > { @@ -86,6 +94,8 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) adminPassword, debugLog, debugDisplay, + scriptDebug, + environmentType, } = options; let { adminEmail } = options; @@ -102,11 +112,13 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) adminPassword === undefined && adminEmail === undefined && debugLog === undefined && - debugDisplay === undefined + debugDisplay === undefined && + scriptDebug === undefined && + environmentType === undefined ) { throw new LoggerError( __( - 'At least one option (--name, --domain, --https, --php, --wp, --runtime, --file-access, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --runtime, --file-access, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display, --script-debug, --environment-type) is required.' ) ); } @@ -204,6 +216,9 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) const debugLogChanged = debugLog !== undefined && debugLog !== site.enableDebugLog; const debugDisplayChanged = debugDisplay !== undefined && debugDisplay !== site.enableDebugDisplay; + const scriptDebugChanged = scriptDebug !== undefined && scriptDebug !== site.enableScriptDebug; + const environmentTypeChanged = + environmentType !== undefined && environmentType !== getWpEnvironmentType( site ); const hasChanges = nameChanged || @@ -216,7 +231,9 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) xdebugChanged || credentialsChanged || debugLogChanged || - debugDisplayChanged; + debugDisplayChanged || + scriptDebugChanged || + environmentTypeChanged; if ( ! hasChanges ) { throw new LoggerError( __( 'No changes to apply. The site already has the specified settings.' ) @@ -234,6 +251,8 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) credentialsChanged, debugLogChanged, debugDisplayChanged, + scriptDebugChanged, + environmentTypeChanged, } ); const oldDomain = site.customDomain; @@ -281,6 +300,12 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) if ( debugDisplayChanged ) { foundSite.enableDebugDisplay = debugDisplay; } + if ( scriptDebugChanged ) { + foundSite.enableScriptDebug = scriptDebug; + } + if ( environmentTypeChanged ) { + foundSite.environmentType = environmentType; + } await saveCliConfig( cliConfig ); site = foundSite; @@ -446,6 +471,15 @@ export const registerCommand = ( yargs: StudioArgv ) => { .option( 'debug-display', { type: 'boolean', description: __( 'Enable WP_DEBUG_DISPLAY' ), + } ) + .option( 'script-debug', { + type: 'boolean', + description: __( 'Enable SCRIPT_DEBUG' ), + } ) + .option( 'environment-type', { + type: 'string', + description: __( 'Set WP_ENVIRONMENT_TYPE' ), + choices: WP_ENVIRONMENT_TYPES, } ); }, handler: async ( argv ) => { @@ -464,6 +498,8 @@ export const registerCommand = ( yargs: StudioArgv ) => { adminEmail: argv.adminEmail, debugLog: argv.debugLog, debugDisplay: argv.debugDisplay, + scriptDebug: argv.scriptDebug, + environmentType: wpEnvironmentTypeSchema.optional().parse( argv.environmentType ), } ); } catch ( error ) { if ( error instanceof LoggerError ) { diff --git a/apps/cli/commands/config/tests/get.test.ts b/apps/cli/commands/config/tests/get.test.ts index 9a464bb06e..88cffc5963 100644 --- a/apps/cli/commands/config/tests/get.test.ts +++ b/apps/cli/commands/config/tests/get.test.ts @@ -28,6 +28,8 @@ describe( 'CLI: studio config get', () => { adminEmail: 'admin@example.com', enableDebugLog: true, enableDebugDisplay: false, + enableScriptDebug: true, + environmentType: 'development' as const, }; beforeEach( () => { @@ -148,6 +150,8 @@ describe( 'CLI: studio config get', () => { 'admin-email': 'admin@example.com', 'debug-log': true, 'debug-display': false, + 'script-debug': true, + 'environment-type': 'development', }, null, 2 @@ -183,6 +187,8 @@ describe( 'CLI: studio config get', () => { 'admin-email': null, 'debug-log': false, 'debug-display': false, + 'script-debug': false, + 'environment-type': 'local', }, null, 2 diff --git a/apps/cli/commands/config/tests/set.test.ts b/apps/cli/commands/config/tests/set.test.ts index 2b90619629..bc57489553 100644 --- a/apps/cli/commands/config/tests/set.test.ts +++ b/apps/cli/commands/config/tests/set.test.ts @@ -109,7 +109,7 @@ describe( 'CLI: studio config set', () => { describe( 'Validation', () => { it( 'should throw when no options provided', async () => { await expect( runCommand( testSitePath, {} ) ).rejects.toThrow( - 'At least one option (--name, --domain, --https, --php, --wp, --runtime, --file-access, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --runtime, --file-access, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display, --script-debug, --environment-type) is required.' ); } ); diff --git a/apps/cli/lib/native-php/blueprints.ts b/apps/cli/lib/native-php/blueprints.ts index f6c36dfb85..dca869fa24 100644 --- a/apps/cli/lib/native-php/blueprints.ts +++ b/apps/cli/lib/native-php/blueprints.ts @@ -4,6 +4,7 @@ import { createBlueprintTempDir, removeBlueprintTempDir, } from '@studio/common/lib/blueprint-bundle'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { getBlueprintsPharPath, getPhpBinaryPath } from 'cli/lib/dependency-management/paths'; import { runPhpCommand } from './php-process'; import type { NativePhpSupportedVersion } from '@studio/common/lib/php-binary-metadata'; @@ -35,6 +36,10 @@ export async function runBlueprint( WP_DEBUG: enableDebugLog || enableDebugDisplay, WP_DEBUG_LOG: enableDebugLog, WP_DEBUG_DISPLAY: enableDebugDisplay, + // SCRIPT_DEBUG is independent of WP_DEBUG in WordPress, so it must not + // feed the WP_DEBUG expression above. + SCRIPT_DEBUG: config.enableScriptDebug ?? false, + WP_ENVIRONMENT_TYPE: getWpEnvironmentType( config ), }; blueprint.contents.constants = { diff --git a/apps/cli/lib/native-php/site-setup.ts b/apps/cli/lib/native-php/site-setup.ts index 81aa27e572..1c381b49e4 100644 --- a/apps/cli/lib/native-php/site-setup.ts +++ b/apps/cli/lib/native-php/site-setup.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { DEFAULT_LOCALE } from '@studio/common/lib/locale'; import { escapePhpSingleQuotedString } from '@studio/common/lib/mu-plugins'; import { decodePassword } from '@studio/common/lib/passwords'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { getWpCliPharPath } from 'cli/lib/dependency-management/paths'; import { runPhpCommand } from './php-process'; import { getFullyResolvedTmpDirPath } from './tmp-dir'; @@ -18,7 +19,10 @@ export async function ensureWpConfig( phpVersion: NativePhpSupportedVersion, signal: AbortSignal, wpConfigTransformerPath: string, - config?: Pick< ServerConfig, 'enableDebugLog' | 'enableDebugDisplay' > + config?: Pick< + ServerConfig, + 'enableDebugLog' | 'enableDebugDisplay' | 'enableScriptDebug' | 'environmentType' + > ): Promise< void > { const wpConfigPath = path.join( siteFolder, 'wp-config.php' ); const wpConfigSamplePath = path.join( siteFolder, 'wp-config-sample.php' ); @@ -45,6 +49,10 @@ $transformer->to_file( $wp_config_path ); WP_DEBUG: enableDebugLog || enableDebugDisplay, WP_DEBUG_LOG: enableDebugLog, WP_DEBUG_DISPLAY: enableDebugDisplay, + // SCRIPT_DEBUG is independent of WP_DEBUG in WordPress, so it must not + // feed the WP_DEBUG expression above. + SCRIPT_DEBUG: config?.enableScriptDebug ?? false, + WP_ENVIRONMENT_TYPE: getWpEnvironmentType( config ?? {} ), }; try { diff --git a/apps/cli/lib/native-php/tests/site-setup.test.ts b/apps/cli/lib/native-php/tests/site-setup.test.ts new file mode 100644 index 0000000000..cd439ea844 --- /dev/null +++ b/apps/cli/lib/native-php/tests/site-setup.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ensureWpConfig } from 'cli/lib/native-php/site-setup'; + +const runPhpCommand = vi.hoisted( () => vi.fn() ); + +vi.mock( 'cli/lib/native-php/php-process', () => ( { runPhpCommand } ) ); +vi.mock( 'cli/lib/dependency-management/paths', () => ( { + getWpCliPharPath: () => '/wp-cli.phar', +} ) ); + +// The constants are passed to PHP as the third positional arg, JSON-encoded. +async function getWrittenConstants( + config?: Parameters< typeof ensureWpConfig >[ 4 ] +): Promise< Record< string, unknown > > { + runPhpCommand.mockClear(); + await ensureWpConfig( + '/nonexistent-site', + '8.4', + new AbortController().signal, + '/wp-config-transformer.php', + config + ); + const args = runPhpCommand.mock.calls[ 0 ][ 0 ] as string[]; + return JSON.parse( args[ args.length - 1 ] ); +} + +describe( 'ensureWpConfig', () => { + beforeEach( () => { + runPhpCommand.mockResolvedValue( undefined ); + } ); + + it( 'defaults both new constants when no config is supplied', async () => { + const constants = await getWrittenConstants(); + + expect( constants.SCRIPT_DEBUG ).toBe( false ); + expect( constants.WP_ENVIRONMENT_TYPE ).toBe( 'local' ); + } ); + + it( 'writes SCRIPT_DEBUG when script debug is enabled', async () => { + const constants = await getWrittenConstants( { enableScriptDebug: true } ); + + expect( constants.SCRIPT_DEBUG ).toBe( true ); + } ); + + // SCRIPT_DEBUG is independent of WP_DEBUG in WordPress. Enabling it must not + // turn on WP_DEBUG as a side effect. + it( 'does not enable WP_DEBUG when only SCRIPT_DEBUG is enabled', async () => { + const constants = await getWrittenConstants( { enableScriptDebug: true } ); + + expect( constants.WP_DEBUG ).toBe( false ); + expect( constants.WP_DEBUG_LOG ).toBe( false ); + expect( constants.WP_DEBUG_DISPLAY ).toBe( false ); + } ); + + it( 'writes the configured environment type', async () => { + const constants = await getWrittenConstants( { environmentType: 'staging' } ); + + expect( constants.WP_ENVIRONMENT_TYPE ).toBe( 'staging' ); + } ); + + it( 'still derives WP_DEBUG from the debug log and display flags', async () => { + const constants = await getWrittenConstants( { enableDebugLog: true } ); + + expect( constants.WP_DEBUG ).toBe( true ); + expect( constants.WP_DEBUG_LOG ).toBe( true ); + } ); +} ); diff --git a/apps/cli/lib/types/wordpress-server-ipc.ts b/apps/cli/lib/types/wordpress-server-ipc.ts index 6d1b0692f5..b1a0011553 100644 --- a/apps/cli/lib/types/wordpress-server-ipc.ts +++ b/apps/cli/lib/types/wordpress-server-ipc.ts @@ -1,4 +1,5 @@ import { siteFileAccessSchema } from '@studio/common/lib/site-file-access'; +import { wpEnvironmentTypeSchema } from '@studio/common/lib/wp-environment-type'; import { z } from 'zod'; import type { WordPressInstallMode } from '@wp-playground/wordpress'; @@ -32,6 +33,8 @@ export const serverConfigSchema = z.object( { enableXdebug: z.boolean().optional(), enableDebugLog: z.boolean().optional(), enableDebugDisplay: z.boolean().optional(), + enableScriptDebug: z.boolean().optional(), + environmentType: wpEnvironmentTypeSchema.optional(), blueprint: z .object( { contents: z.any(), // Blueprint type is complex, allow any for now diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index 13fc2f2def..082625e13e 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -20,6 +20,7 @@ import { SITE_RUNTIME_PLAYGROUND, type SiteRuntime, } from '@studio/common/lib/site-runtime'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { SiteCommandLoggerAction } from '@studio/common/logger-actions'; import { __ } from '@wordpress/i18n'; import { z } from 'zod'; @@ -198,6 +199,12 @@ function buildServerConfig( serverConfig.enableDebugDisplay = true; } + if ( site.enableScriptDebug ) { + serverConfig.enableScriptDebug = true; + } + + serverConfig.environmentType = getWpEnvironmentType( site ); + return serverConfig; } diff --git a/apps/cli/playground-server-child.ts b/apps/cli/playground-server-child.ts index 60e3c46118..c7ecf4b5c7 100644 --- a/apps/cli/playground-server-child.ts +++ b/apps/cli/playground-server-child.ts @@ -25,6 +25,7 @@ import { import { formatPlaygroundCliMessage } from '@studio/common/lib/playground-cli-messages'; import { sequential } from '@studio/common/lib/sequential'; import { isWordPressDevVersion } from '@studio/common/lib/wordpress-version-utils'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { BlueprintBundle } from '@wp-playground/blueprints'; import { runCLI, RunCLIArgs, RunCLIServer } from '@wp-playground/cli'; import { @@ -291,6 +292,10 @@ async function getBaseRunCLIArgs( WP_DEBUG: enableDebugLog || enableDebugDisplay, WP_DEBUG_LOG: enableDebugLog, WP_DEBUG_DISPLAY: enableDebugDisplay, + // SCRIPT_DEBUG is independent of WP_DEBUG in WordPress, so it must not + // feed the WP_DEBUG expression above. + SCRIPT_DEBUG: config.enableScriptDebug ?? false, + WP_ENVIRONMENT_TYPE: getWpEnvironmentType( config ), }; let blueprintBundle: BlueprintBundle | undefined; diff --git a/apps/local/src/index.ts b/apps/local/src/index.ts index 95c2107f42..954eacaeda 100644 --- a/apps/local/src/index.ts +++ b/apps/local/src/index.ts @@ -55,6 +55,7 @@ import { fetchStudioAssistantQuota } from '@studio/common/lib/studio-assistant-q import { fetchSyncableSites } from '@studio/common/lib/sync/sync-api'; import { detectInstalledApps } from '@studio/common/lib/user-settings/installed-apps'; import { isWordPressDevVersion } from '@studio/common/lib/wordpress-version-utils'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { cleanupBlueprintTempDir, extractBlueprintUpload, @@ -158,6 +159,8 @@ function toSiteDetails( site: SiteListItem ) { enableXdebug: site.enableXdebug, enableDebugLog: site.enableDebugLog, enableDebugDisplay: site.enableDebugDisplay, + enableScriptDebug: site.enableScriptDebug, + environmentType: site.environmentType, siteIcon: null, }; } @@ -699,6 +702,12 @@ export async function startLocalServer( options: LocalServerOptions ): Promise< if ( ( updated.enableDebugDisplay ?? false ) !== ( current.enableDebugDisplay ?? false ) ) { options.debugDisplay = updated.enableDebugDisplay ?? false; } + if ( ( updated.enableScriptDebug ?? false ) !== ( current.enableScriptDebug ?? false ) ) { + options.scriptDebug = updated.enableScriptDebug ?? false; + } + if ( getWpEnvironmentType( updated ) !== getWpEnvironmentType( current ) ) { + options.environmentType = getWpEnvironmentType( updated ); + } // More than path + siteId means a real change to apply. if ( Object.keys( options ).length > 2 ) { diff --git a/apps/studio/src/components/content-tab-settings.tsx b/apps/studio/src/components/content-tab-settings.tsx index e8a61c9b9d..df4de60ca5 100644 --- a/apps/studio/src/components/content-tab-settings.tsx +++ b/apps/studio/src/components/content-tab-settings.tsx @@ -1,6 +1,14 @@ import { decodePassword } from '@studio/common/lib/passwords'; import { getSiteFileAccess, SITE_FILE_ACCESS_ALL_FILES } from '@studio/common/lib/site-file-access'; import { getSiteRuntime, SITE_RUNTIME_NATIVE_PHP } from '@studio/common/lib/site-runtime'; +import { + getWpEnvironmentType, + WP_ENVIRONMENT_TYPE_DEVELOPMENT, + WP_ENVIRONMENT_TYPE_LOCAL, + WP_ENVIRONMENT_TYPE_PRODUCTION, + WP_ENVIRONMENT_TYPE_STAGING, + type WpEnvironmentType, +} from '@studio/common/lib/wp-environment-type'; import { getClosestSupportedPhpVersion } from '@studio/common/types/php-versions'; import { DropdownMenu, @@ -113,6 +121,14 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) const sandboxLabel = __( 'Sandbox' ); const runtimeLabel = isNativePhpRuntime ? nativeLabel : sandboxLabel; + const environmentTypeLabels: Record< WpEnvironmentType, string > = { + [ WP_ENVIRONMENT_TYPE_LOCAL ]: __( 'Local' ), + [ WP_ENVIRONMENT_TYPE_DEVELOPMENT ]: __( 'Development' ), + [ WP_ENVIRONMENT_TYPE_STAGING ]: __( 'Staging' ), + [ WP_ENVIRONMENT_TYPE_PRODUCTION ]: __( 'Production' ), + }; + const environmentTypeLabel = environmentTypeLabels[ getWpEnvironmentType( selectedSite ) ]; + return (
@@ -279,6 +295,13 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) { /* translators: status value for the Debug display setting on the site settings screen */ } { selectedSite.enableDebugDisplay ? __( 'Enabled' ) : __( 'Disabled' ) } + + { /* translators: status value for the Script debug setting on the site settings screen */ } + { selectedSite.enableScriptDebug ? __( 'Enabled' ) : __( 'Disabled' ) } + + + { environmentTypeLabel } +

{ __( 'WP Admin' ) }

diff --git a/apps/studio/src/components/tests/content-tab-settings.test.tsx b/apps/studio/src/components/tests/content-tab-settings.test.tsx index 4b054a074f..3024b3777d 100644 --- a/apps/studio/src/components/tests/content-tab-settings.test.tsx +++ b/apps/studio/src/components/tests/content-tab-settings.test.tsx @@ -159,8 +159,8 @@ describe( 'ContentTabSettings', () => { ).toHaveTextContent( 'localhost:8881' ); expect( screen.getByText( 'HTTPS' ) ).toBeVisible(); expect( screen.getByText( 'Xdebug' ) ).toBeVisible(); - // HTTPS, Xdebug, Debug log, and Debug display show "Disabled" - expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 4 ); + // HTTPS, Xdebug, Debug log, Debug display, and Script debug show "Disabled" + expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 5 ); expect( screen.getByRole( 'button', { name: 'Copy local path to clipboard' } ) ).toBeVisible(); expect( screen.getByText( '7.7.7' ) ).toBeVisible(); expect( diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 59aa476a8e..782eb8e93d 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -93,6 +93,7 @@ import { shouldExcludeFromSync } from '@studio/common/lib/sync/exclude-from-sync import { shouldLimitDepth } from '@studio/common/lib/sync/tree-utils'; import { getSessionsDirectory } from '@studio/common/lib/well-known-paths'; import { isWordPressDevVersion } from '@studio/common/lib/wordpress-version-utils'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { cleanupBlueprintTempDir as cleanupBlueprintTempDirShared, extractBlueprintBundle as extractBlueprintBundleShared, @@ -965,6 +966,14 @@ export async function updateSite( options.debugDisplay = updatedSite.enableDebugDisplay ?? false; } + if ( updatedSite.enableScriptDebug !== currentSite.enableScriptDebug ) { + options.scriptDebug = updatedSite.enableScriptDebug ?? false; + } + + if ( getWpEnvironmentType( updatedSite ) !== getWpEnvironmentType( currentSite ) ) { + options.environmentType = getWpEnvironmentType( updatedSite ); + } + const hasCliChanges = Object.keys( options ).length > 2; if ( hasCliChanges ) { diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts index 6164993ecb..54dbcf41fa 100644 --- a/apps/studio/src/ipc-types.d.ts +++ b/apps/studio/src/ipc-types.d.ts @@ -7,6 +7,10 @@ interface ShowNotificationOptions extends Electron.NotificationConstructorOption type SiteRuntime = 'playground' | 'native-php'; type SiteFileAccess = 'site-directory' | 'all-files'; +// Mirrors WpEnvironmentType in @studio/common/lib/wp-environment-type. Declared +// inline because this file is a global declaration file — adding an import +// would turn it into a module and drop these globals. +type WpEnvironmentType = 'local' | 'development' | 'staging' | 'production'; interface StoppedSiteDetails { running: false; @@ -45,6 +49,8 @@ interface StoppedSiteDetails { enableXdebug?: boolean; enableDebugLog?: boolean; enableDebugDisplay?: boolean; + enableScriptDebug?: boolean; + environmentType?: WpEnvironmentType; sortOrder?: number; landingPage?: string; runtime?: SiteRuntime; diff --git a/apps/studio/src/modules/site-settings/edit-site-details.tsx b/apps/studio/src/modules/site-settings/edit-site-details.tsx index 8c82082620..e377ce4f2c 100644 --- a/apps/studio/src/modules/site-settings/edit-site-details.tsx +++ b/apps/studio/src/modules/site-settings/edit-site-details.tsx @@ -22,6 +22,14 @@ import { SITE_RUNTIME_PLAYGROUND, type SiteRuntime, } from '@studio/common/lib/site-runtime'; +import { + getWpEnvironmentType, + WP_ENVIRONMENT_TYPE_DEVELOPMENT, + WP_ENVIRONMENT_TYPE_LOCAL, + WP_ENVIRONMENT_TYPE_PRODUCTION, + WP_ENVIRONMENT_TYPE_STAGING, + type WpEnvironmentType, +} from '@studio/common/lib/wp-environment-type'; import { getClosestSupportedPhpVersion, RecommendedPHPVersion, @@ -79,6 +87,12 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const [ enableDebugDisplay, setEnableDebugDisplay ] = useState( selectedSite?.enableDebugDisplay ?? false ); + const [ enableScriptDebug, setEnableScriptDebug ] = useState( + selectedSite?.enableScriptDebug ?? false + ); + const [ environmentType, setEnvironmentType ] = useState< WpEnvironmentType >( + getWpEnvironmentType( selectedSite ?? {} ) + ); const [ xdebugEnabledSite, setXdebugEnabledSite ] = useState< SiteDetails | null >( null ); const [ adminUsername, setAdminUsername ] = useState( selectedSite?.adminUsername ?? 'admin' ); const [ adminPassword, setAdminPassword ] = useState( @@ -162,6 +176,15 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = () => activeTab === 'general' || activeTab === 'debugging', [ activeTab ] ); + const environmentTypeOptions = useMemo< { label: string; value: WpEnvironmentType }[] >( + () => [ + { label: __( 'Local' ), value: WP_ENVIRONMENT_TYPE_LOCAL }, + { label: __( 'Development' ), value: WP_ENVIRONMENT_TYPE_DEVELOPMENT }, + { label: __( 'Staging' ), value: WP_ENVIRONMENT_TYPE_STAGING }, + { label: __( 'Production' ), value: WP_ENVIRONMENT_TYPE_PRODUCTION }, + ], + [ __ ] + ); useEffect( () => { getIpcApi() @@ -208,7 +231,9 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = ( decodePassword( selectedSite.adminPassword ?? '' ) || 'password' ) === adminPassword && ( selectedSite.adminEmail || 'admin@localhost.com' ) === adminEmail && !! selectedSite.enableDebugLog === enableDebugLog && - !! selectedSite.enableDebugDisplay === enableDebugDisplay; + !! selectedSite.enableDebugDisplay === enableDebugDisplay && + !! selectedSite.enableScriptDebug === enableScriptDebug && + getWpEnvironmentType( selectedSite ) === environmentType; const hasValidationErrors = ! selectedSite || ! siteName.trim() || @@ -237,6 +262,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = setAdminEmail( selectedSite.adminEmail || 'admin@localhost.com' ); setEnableDebugLog( selectedSite.enableDebugLog ?? false ); setEnableDebugDisplay( selectedSite.enableDebugDisplay ?? false ); + setEnableScriptDebug( selectedSite.enableScriptDebug ?? false ); + setEnvironmentType( getWpEnvironmentType( selectedSite ) ); }, [ selectedSite, getEffectiveWpVersion, @@ -247,6 +274,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = setCustomDomainError, setEnableDebugDisplay, setEnableDebugLog, + setEnableScriptDebug, + setEnvironmentType, setEnableHttps, setEnableXdebug, setErrorUpdatingWpVersion, @@ -272,6 +301,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const hasDebugLogChanged = enableDebugLog !== ( selectedSite.enableDebugLog ?? false ); const hasDebugDisplayChanged = enableDebugDisplay !== ( selectedSite.enableDebugDisplay ?? false ); + const hasScriptDebugChanged = enableScriptDebug !== ( selectedSite.enableScriptDebug ?? false ); + const hasEnvironmentTypeChanged = environmentType !== getWpEnvironmentType( selectedSite ); const hasDomainChanged = Boolean( selectedSite.customDomain ) !== useCustomDomain || ( useCustomDomain && customDomain !== selectedSite.customDomain ); @@ -295,6 +326,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = credentialsChanged: hasCredentialsChanged, debugLogChanged: hasDebugLogChanged, debugDisplayChanged: hasDebugDisplayChanged, + scriptDebugChanged: hasScriptDebugChanged, + environmentTypeChanged: hasEnvironmentTypeChanged, } ); setNeedsRestart( needsRestart ); @@ -322,6 +355,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = adminEmail, enableDebugLog, enableDebugDisplay, + enableScriptDebug, + environmentType, }, hasWpVersionChanged ? selectedWpVersion : undefined ); @@ -757,6 +792,57 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = ) }
+ +
+
+ setEnableScriptDebug( e.target.checked ) } + disabled={ isEditingSite } + /> + +
+
+ { __( + 'Load the development versions of core CSS and JavaScript instead of the minified files by setting the SCRIPT_DEBUG constant. Useful for reading React errors in the block editor.' + ) } +
+
+ +
+ +
+ { __( + 'Sets the WP_ENVIRONMENT_TYPE constant, which determines the value returned by wp_get_environment_type(). Plugins and themes use it to vary their behavior between local, staging, and production sites.' + ) } +
+
) } { name === 'skills' && selectedSite && ( diff --git a/apps/ui/src/components/site-fields/index.ts b/apps/ui/src/components/site-fields/index.ts index 0fdd8d0cbe..4ab260c586 100644 --- a/apps/ui/src/components/site-fields/index.ts +++ b/apps/ui/src/components/site-fields/index.ts @@ -16,11 +16,18 @@ import { isWordPressBetaVersion, isWordPressDevVersion, } from '@studio/common/lib/wordpress-version-utils'; +import { + WP_ENVIRONMENT_TYPE_DEVELOPMENT, + WP_ENVIRONMENT_TYPE_LOCAL, + WP_ENVIRONMENT_TYPE_PRODUCTION, + WP_ENVIRONMENT_TYPE_STAGING, +} from '@studio/common/lib/wp-environment-type'; import { SupportedPHPVersions } from '@studio/common/types/php-versions'; import { __ } from '@wordpress/i18n'; import { WpVersionControl } from '@/components/site-fields/wp-version-control'; import type { WpVersionOption } from '@/components/site-fields/wp-version-control'; import type { WordPressVersion } from '@studio/common/lib/wordpress-versions'; +import type { WpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import type { SupportedPHPVersion } from '@studio/common/types/php-versions'; import type { Field, Option } from '@wordpress/dataviews'; @@ -29,6 +36,13 @@ const PHP_VERSION_ELEMENTS = SupportedPHPVersions.map( ( version ) => ( { label: version, } ) ); +const ENVIRONMENT_TYPE_ELEMENTS = [ + { value: WP_ENVIRONMENT_TYPE_LOCAL, label: __( 'Local' ) }, + { value: WP_ENVIRONMENT_TYPE_DEVELOPMENT, label: __( 'Development' ) }, + { value: WP_ENVIRONMENT_TYPE_STAGING, label: __( 'Staging' ) }, + { value: WP_ENVIRONMENT_TYPE_PRODUCTION, label: __( 'Production' ) }, +]; + export function siteNameField< T extends { name: string } >(): Field< T > { return { id: 'name', @@ -268,3 +282,28 @@ export function enableDebugDisplayField< T extends { enableDebugDisplay: boolean description: __( 'Display PHP errors and warnings directly in the browser.' ), }; } + +export function enableScriptDebugField< T extends { enableScriptDebug: boolean } >(): Field< T > { + return { + id: 'enableScriptDebug', + type: 'boolean', + label: __( 'Enable script debug' ), + description: __( + 'Load the development versions of core CSS and JavaScript instead of the minified files. Useful for reading React errors in the block editor.' + ), + }; +} + +export function environmentTypeField< + T extends { environmentType: WpEnvironmentType }, +>(): Field< T > { + return { + id: 'environmentType', + type: 'text', + label: __( 'Environment type' ), + elements: ENVIRONMENT_TYPE_ELEMENTS, + description: __( + 'Sets the value returned by wp_get_environment_type(). Plugins and themes use it to vary their behavior between local, staging, and production sites.' + ), + }; +} diff --git a/apps/ui/src/components/site-settings-view/index.tsx b/apps/ui/src/components/site-settings-view/index.tsx index 1b977a6a80..c8107ed4f4 100644 --- a/apps/ui/src/components/site-settings-view/index.tsx +++ b/apps/ui/src/components/site-settings-view/index.tsx @@ -1,6 +1,7 @@ import { DEFAULT_WORDPRESS_VERSION } from '@studio/common/constants'; import { generateCustomDomainFromSiteName } from '@studio/common/lib/domains'; import { decodePassword, encodePassword } from '@studio/common/lib/passwords'; +import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { RecommendedPHPVersion } from '@studio/common/types/php-versions'; import { CheckboxControl } from '@wordpress/components'; import { DataForm, useFormValidity } from '@wordpress/dataviews'; @@ -16,7 +17,9 @@ import { customDomainToggleField, enableDebugDisplayField, enableDebugLogField, + enableScriptDebugField, enableXdebugField, + environmentTypeField, phpVersionField, siteNameField, wpVersionField, @@ -28,6 +31,7 @@ import { useWordPressVersions, useWpVersion } from '@/data/queries/use-wordpress import { useOffline } from '@/hooks/use-offline'; import styles from './style.module.css'; import type { SiteDetails } from '@/data/core'; +import type { WpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import type { SupportedPHPVersion } from '@studio/common/types/php-versions'; import type { DataFormControlProps, Field, Form } from '@wordpress/dataviews'; import type { FormEvent } from 'react'; @@ -49,6 +53,8 @@ interface FormData { enableXdebug: boolean; enableDebugLog: boolean; enableDebugDisplay: boolean; + enableScriptDebug: boolean; + environmentType: WpEnvironmentType; } function getEffectiveWpVersion( site: SiteDetails, installedVersion?: string ): string { @@ -76,6 +82,8 @@ function initialFormData( site: SiteDetails, installedWpVersion?: string ): Form enableXdebug: site.enableXdebug ?? false, enableDebugLog: site.enableDebugLog ?? false, enableDebugDisplay: site.enableDebugDisplay ?? false, + enableScriptDebug: site.enableScriptDebug ?? false, + environmentType: getWpEnvironmentType( site ), }; } @@ -169,6 +177,8 @@ export function SiteSettingsForm( { site, activeTab }: { site: SiteDetails; acti enableXdebugField< FormData >( { conflictingSiteName: xdebugConflictSiteName } ), enableDebugLogField< FormData >(), enableDebugDisplayField< FormData >(), + enableScriptDebugField< FormData >(), + environmentTypeField< FormData >(), ], [ existingDomainNames, installedWpVersion, isOffline, wpVersions, xdebugConflictSiteName ] ); @@ -199,7 +209,13 @@ export function SiteSettingsForm( { site, activeTab }: { site: SiteDetails; acti const debuggingForm = useMemo< Form >( () => ( { layout: { type: 'regular', labelPosition: 'top' }, - fields: [ 'enableXdebug', 'enableDebugLog', 'enableDebugDisplay' ], + fields: [ + 'enableXdebug', + 'enableDebugLog', + 'enableDebugDisplay', + 'enableScriptDebug', + 'environmentType', + ], } ), [] ); @@ -261,6 +277,8 @@ export function SiteSettingsForm( { site, activeTab }: { site: SiteDetails; acti enableXdebug: data.enableXdebug, enableDebugLog: data.enableDebugLog, enableDebugDisplay: data.enableDebugDisplay, + enableScriptDebug: data.enableScriptDebug, + environmentType: data.environmentType, }; // Only forward the version when the user actually changed it — same as // the legacy settings modal — so unrelated saves of a pinned site don't diff --git a/apps/ui/src/data/core/types.ts b/apps/ui/src/data/core/types.ts index d02d7c5203..10fa276913 100644 --- a/apps/ui/src/data/core/types.ts +++ b/apps/ui/src/data/core/types.ts @@ -11,6 +11,7 @@ import type { StudioAssistantQuota } from '@studio/common/lib/studio-assistant-q import type { SupportedEditor } from '@studio/common/lib/user-settings/editor'; import type { SupportedTerminal } from '@studio/common/lib/user-settings/terminal'; import type { WordPressVersion } from '@studio/common/lib/wordpress-versions'; +import type { WpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import type { SupportedPHPVersion } from '@studio/common/types/php-versions'; import type { Snapshot } from '@studio/common/types/snapshot'; import type { SyncSite } from '@studio/common/types/sync'; @@ -73,6 +74,8 @@ export interface SiteDetails { enableXdebug?: boolean; enableDebugLog?: boolean; enableDebugDisplay?: boolean; + enableScriptDebug?: boolean; + environmentType?: WpEnvironmentType; sortOrder?: number; // True for sites that were running when the app quit with the // "Stop, restart on next launch" behavior; the renderer starts them on boot. diff --git a/packages/common/lib/cli-events.ts b/packages/common/lib/cli-events.ts index db4db5e88f..74042f3eef 100644 --- a/packages/common/lib/cli-events.ts +++ b/packages/common/lib/cli-events.ts @@ -8,6 +8,7 @@ import { z } from 'zod'; import { authTokenSchema } from '@studio/common/lib/auth-token-schema'; import { siteFileAccessSchema } from '@studio/common/lib/site-file-access'; import { siteRuntimeSchema } from '@studio/common/lib/site-runtime'; +import { wpEnvironmentTypeSchema } from '@studio/common/lib/wp-environment-type'; import { snapshotSchema } from '@studio/common/types/snapshot'; /** @@ -31,6 +32,8 @@ export const siteDetailsSchema = z.object( { enableXdebug: z.boolean().optional(), enableDebugLog: z.boolean().optional(), enableDebugDisplay: z.boolean().optional(), + enableScriptDebug: z.boolean().optional(), + environmentType: wpEnvironmentTypeSchema.optional(), technicalSiteDirectory: z.string().optional(), runtimeBlueprintPath: z.string().optional(), landingPage: z.string().optional(), diff --git a/packages/common/lib/site-needs-restart.ts b/packages/common/lib/site-needs-restart.ts index 9c557bf6af..864583afe5 100644 --- a/packages/common/lib/site-needs-restart.ts +++ b/packages/common/lib/site-needs-restart.ts @@ -9,6 +9,8 @@ export interface SiteSettingChanges { credentialsChanged?: boolean; debugLogChanged?: boolean; debugDisplayChanged?: boolean; + scriptDebugChanged?: boolean; + environmentTypeChanged?: boolean; } export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { @@ -23,6 +25,8 @@ export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { credentialsChanged, debugLogChanged, debugDisplayChanged, + scriptDebugChanged, + environmentTypeChanged, } = changes; return !! ( @@ -35,6 +39,8 @@ export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { xdebugChanged || credentialsChanged || debugLogChanged || - debugDisplayChanged + debugDisplayChanged || + scriptDebugChanged || + environmentTypeChanged ); } diff --git a/packages/common/lib/tests/site-needs-restart.test.ts b/packages/common/lib/tests/site-needs-restart.test.ts index 0a8471474c..8d52116ed9 100644 --- a/packages/common/lib/tests/site-needs-restart.test.ts +++ b/packages/common/lib/tests/site-needs-restart.test.ts @@ -37,6 +37,14 @@ describe( 'siteNeedsRestart', () => { expect( siteNeedsRestart( { xdebugChanged: true } ) ).toBe( true ); } ); + it( 'returns true when script debug changed', () => { + expect( siteNeedsRestart( { scriptDebugChanged: true } ) ).toBe( true ); + } ); + + it( 'returns true when environment type changed', () => { + expect( siteNeedsRestart( { environmentTypeChanged: true } ) ).toBe( true ); + } ); + it( 'returns true when multiple settings changed', () => { expect( siteNeedsRestart( { diff --git a/packages/common/lib/tests/wp-environment-type.test.ts b/packages/common/lib/tests/wp-environment-type.test.ts new file mode 100644 index 0000000000..25077cd633 --- /dev/null +++ b/packages/common/lib/tests/wp-environment-type.test.ts @@ -0,0 +1,23 @@ +import { getWpEnvironmentType, wpEnvironmentTypeSchema } from '../wp-environment-type'; + +describe( 'getWpEnvironmentType', () => { + it( 'falls back to "local" when the site has no stored value', () => { + expect( getWpEnvironmentType( {} ) ).toBe( 'local' ); + } ); + + it( 'returns the stored value when set', () => { + expect( getWpEnvironmentType( { environmentType: 'production' } ) ).toBe( 'production' ); + } ); +} ); + +describe( 'wpEnvironmentTypeSchema', () => { + it( 'accepts the four values WordPress recognizes', () => { + for ( const value of [ 'local', 'development', 'staging', 'production' ] ) { + expect( wpEnvironmentTypeSchema.parse( value ) ).toBe( value ); + } + } ); + + it( 'rejects anything else', () => { + expect( () => wpEnvironmentTypeSchema.parse( 'prod' ) ).toThrow(); + } ); +} ); diff --git a/packages/common/lib/wp-environment-type.ts b/packages/common/lib/wp-environment-type.ts new file mode 100644 index 0000000000..5e6f4de650 --- /dev/null +++ b/packages/common/lib/wp-environment-type.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +export const WP_ENVIRONMENT_TYPE_LOCAL = 'local' as const; +export const WP_ENVIRONMENT_TYPE_DEVELOPMENT = 'development' as const; +export const WP_ENVIRONMENT_TYPE_STAGING = 'staging' as const; +export const WP_ENVIRONMENT_TYPE_PRODUCTION = 'production' as const; + +export const wpEnvironmentTypeSchema = z.enum( [ + WP_ENVIRONMENT_TYPE_LOCAL, + WP_ENVIRONMENT_TYPE_DEVELOPMENT, + WP_ENVIRONMENT_TYPE_STAGING, + WP_ENVIRONMENT_TYPE_PRODUCTION, +] ); +export type WpEnvironmentType = z.infer< typeof wpEnvironmentTypeSchema >; + +export const WP_ENVIRONMENT_TYPES = wpEnvironmentTypeSchema.options; + +// Studio's loader mu-plugin falls back to "local" for sites whose wp-config.php +// doesn't define the constant, so unset sites behave as local today. +export function getWpEnvironmentType( site: { + environmentType?: WpEnvironmentType; +} ): WpEnvironmentType { + return site.environmentType ?? WP_ENVIRONMENT_TYPE_LOCAL; +} diff --git a/packages/common/sites/edit.test.ts b/packages/common/sites/edit.test.ts new file mode 100644 index 0000000000..b8a4715f6a --- /dev/null +++ b/packages/common/sites/edit.test.ts @@ -0,0 +1,30 @@ +import { buildSiteSetArgs } from './edit'; + +const base = { path: '/sites/example', siteId: 'abc123' }; + +describe( 'buildSiteSetArgs', () => { + it( 'forwards only the path when nothing changed', () => { + expect( buildSiteSetArgs( base ) ).toEqual( [ 'site', 'set', '--path', '/sites/example' ] ); + } ); + + it( 'forwards --script-debug when script debug is enabled', () => { + expect( buildSiteSetArgs( { ...base, scriptDebug: true } ) ).toContain( '--script-debug' ); + } ); + + it( 'forwards --no-script-debug when script debug is disabled', () => { + expect( buildSiteSetArgs( { ...base, scriptDebug: false } ) ).toContain( '--no-script-debug' ); + } ); + + it( 'forwards the environment type as a value', () => { + expect( buildSiteSetArgs( { ...base, environmentType: 'staging' } ) ).toEqual( + expect.arrayContaining( [ '--environment-type', 'staging' ] ) + ); + } ); + + it( 'omits both flags when neither is set', () => { + const args = buildSiteSetArgs( { ...base, name: 'Example' } ); + expect( args ).not.toContain( '--script-debug' ); + expect( args ).not.toContain( '--no-script-debug' ); + expect( args ).not.toContain( '--environment-type' ); + } ); +} ); diff --git a/packages/common/sites/edit.ts b/packages/common/sites/edit.ts index c8b1c3e3bc..94fea0d165 100644 --- a/packages/common/sites/edit.ts +++ b/packages/common/sites/edit.ts @@ -1,5 +1,6 @@ import type { SiteFileAccess } from '@studio/common/lib/site-file-access'; import type { SiteMode } from '@studio/common/lib/site-runtime'; +import type { WpEnvironmentType } from '@studio/common/lib/wp-environment-type'; /** Options accepted by the CLI `site set` command. */ export interface EditSiteOptions { @@ -18,6 +19,8 @@ export interface EditSiteOptions { adminEmail?: string; debugLog?: boolean; debugDisplay?: boolean; + scriptDebug?: boolean; + environmentType?: WpEnvironmentType; } /** @@ -66,6 +69,12 @@ export function buildSiteSetArgs( options: EditSiteOptions ): string[] { if ( options.debugDisplay !== undefined ) { args.push( options.debugDisplay ? '--debug-display' : '--no-debug-display' ); } + if ( options.scriptDebug !== undefined ) { + args.push( options.scriptDebug ? '--script-debug' : '--no-script-debug' ); + } + if ( options.environmentType !== undefined ) { + args.push( '--environment-type', options.environmentType ); + } return args; }