+ { __(
+ '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;
}