Skip to content

Commit 0df35c0

Browse files
authored
Merge branch 'trunk' into rsm-1650-codex-build-static-php-cli-binaries
2 parents f94a511 + 854412a commit 0df35c0

19 files changed

Lines changed: 375 additions & 256 deletions

.github/dependabot.yml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ updates:
2525
- dependency-name: "wpcom"
2626
- dependency-name: "wpcom-*"
2727

28-
# WordPress Playground & PHP WASM
29-
- dependency-name: "@wp-playground/*"
30-
- dependency-name: "@php-wasm/*"
31-
3228
# TypeScript ecosystem
3329
- dependency-name: "typescript"
3430
- dependency-name: "typescript-*"
@@ -165,11 +161,6 @@ updates:
165161
- "electron-playwright-helpers"
166162
- "electron-devtools-installer"
167163

168-
wp-playground-php-wasm:
169-
patterns:
170-
- "@wp-playground/*"
171-
- "@php-wasm/*"
172-
173164
wordpress:
174165
patterns:
175166
- "@wordpress/*"
@@ -279,6 +270,8 @@ updates:
279270
- dependency-name: "eslint-plugin-studio"
280271
- dependency-name: "winreg" # v1.2.5 has a known issue: https://github.com/fresc81/node-winreg/issues/65
281272
- dependency-name: "@types/glob" # glob@7 (transitive) has no bundled types; @types/glob@9 is a stub for glob@9+ only
273+
- dependency-name: "@wp-playground/*" # must be pinned exactly and bumped manually in lockstep — version mismatches cause runtime failures and ~450 MB bundle bloat
274+
- dependency-name: "@php-wasm/*" # must be pinned exactly and bumped manually in lockstep — version mismatches cause runtime failures and ~450 MB bundle bloat
282275

283276
# Enable version updates for GitHub Actions
284277
- package-ecosystem: "github-actions"

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ If you've built a substantial new feature — especially one generated with AI a
109109

110110
**Port Conflicts**: Site servers dynamically allocate ports. Don't hardcode port numbers; use the port-finder utility.
111111

112+
**CRITICAL - Playground/PHP-WASM Package Versions**: Always pin `@wp-playground/*` and `@php-wasm/*` packages to **exact versions** (no `^` or `~` ranges) in all `package.json` files. A caret range causes `install:bundle` to resolve a newer version when one publishes, creating a version conflict. npm then installs duplicate copies of all PHP WASM packages nested under the conflicting package's `node_modules/`. The `prune-php-wasm` vite plugin only removes top-level asyncify directories and misses nested copies, resulting in ~450 MB of bloat in the app bundle. More critically, different parts of Studio end up running mismatched Playground/PHP-WASM versions, which can cause subtle and hard-to-diagnose runtime failures in core site operations.
113+
112114
## Detailed Documentation
113115

114116
For in-depth information, see these docs:

apps/cli/commands/site/create.ts

Lines changed: 9 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import crypto from 'crypto';
22
import fs from 'fs';
3-
import os from 'os';
43
import path from 'path';
54
import { confirm, input, password, select } from '@inquirer/prompts';
65
import { SupportedPHPVersions } from '@php-wasm/universal';
@@ -45,11 +44,7 @@ import {
4544
import { fetchWordPressVersions } from '@studio/common/lib/wordpress-versions';
4645
import { SiteCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions';
4746
import { __, sprintf } from '@wordpress/i18n';
48-
import {
49-
isStepDefinition,
50-
type BlueprintV1Declaration,
51-
type StepDefinition,
52-
} from '@wp-playground/blueprints';
47+
import { isStepDefinition, type BlueprintV1Declaration } from '@wp-playground/blueprints';
5348
import { bumpStat, getPlatformMetric } from 'cli/lib/bump-stat';
5449
import {
5550
lockCliConfig,
@@ -279,71 +274,14 @@ export async function runCommand(
279274
const externalPassword = options.adminPassword || blueprintCredentials?.adminPassword;
280275
const adminPassword = externalPassword ? encodePassword( externalPassword ) : createPassword();
281276

282-
const setupSteps: StepDefinition[] = [];
283-
284277
const siteLanguage = await getPreferredSiteLanguage( options.wpVersion );
285278

286-
if ( siteLanguage && siteLanguage !== DEFAULT_LOCALE ) {
287-
// For the 'latest' WP version, try using bundled language packs first to avoid
288-
// a network round-trip. Fall back to the Playground setSiteLanguage step for
289-
// non-latest versions or when bundled packs aren't available.
290-
let isUsingBundledLanguagePacks = false;
291-
if ( options.wpVersion === DEFAULT_WORDPRESS_VERSION ) {
292-
isUsingBundledLanguagePacks = await copyLanguagePackToSite( sitePath, siteLanguage );
293-
}
294-
295-
if ( isUsingBundledLanguagePacks ) {
296-
setupSteps.push(
297-
{
298-
step: 'defineWpConfigConsts',
299-
consts: {
300-
WPLANG: siteLanguage,
301-
},
302-
},
303-
{
304-
step: 'setSiteOptions',
305-
options: {
306-
WPLANG: siteLanguage,
307-
},
308-
}
309-
);
310-
} else if ( isOnlineStatus ) {
311-
setupSteps.push(
312-
{
313-
step: 'setSiteLanguage',
314-
language: siteLanguage,
315-
},
316-
{
317-
step: 'setSiteOptions',
318-
options: {
319-
WPLANG: siteLanguage,
320-
},
321-
}
322-
);
323-
}
324-
}
325-
326-
const hasWpConfig = await pathExists( path.join( sitePath, 'wp-config.php' ) );
327-
const isWordPressDirectoryInitialized = isWordPressDirResult && hasWpConfig;
328-
if ( options.name && ! isWordPressDirectoryInitialized ) {
329-
setupSteps.push( {
330-
step: 'setSiteOptions',
331-
options: {
332-
blogname: options.name,
333-
},
334-
} );
335-
}
336-
337-
if ( setupSteps.length > 0 ) {
338-
if ( ! blueprint ) {
339-
blueprint = {};
340-
// Since we know the user didn't supply a blueprint, we create an empty directory to use as a
341-
// fake location for the `blueprintUri`
342-
const blueprintDir = fs.mkdtempSync( path.join( os.tmpdir(), 'studio-empty-blueprint-' ) );
343-
blueprintUri = path.join( blueprintDir, 'blueprint.json' );
344-
}
345-
const existingSteps = Array.isArray( blueprint.steps ) ? blueprint.steps : [];
346-
blueprint = { ...blueprint, steps: [ ...setupSteps, ...existingSteps ] };
279+
if (
280+
siteLanguage &&
281+
siteLanguage !== DEFAULT_LOCALE &&
282+
options.wpVersion === DEFAULT_WORDPRESS_VERSION
283+
) {
284+
await copyLanguagePackToSite( sitePath, siteLanguage );
347285
}
348286

349287
const siteDetails: SiteData = {
@@ -394,6 +332,7 @@ export async function runCommand(
394332
wpVersion: options.wpVersion,
395333
blueprint,
396334
blueprintUri,
335+
siteLanguage,
397336
} );
398337
logger.reportSuccess( __( 'WordPress server started' ) );
399338

@@ -437,6 +376,7 @@ export async function runCommand(
437376
wpVersion: options.wpVersion,
438377
blueprint,
439378
blueprintUri,
379+
siteLanguage,
440380
} );
441381
logger.reportSuccess( __( 'Blueprint applied successfully' ) );
442382

apps/cli/commands/site/tests/create.test.ts

Lines changed: 21 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { isOnline } from '@studio/common/lib/network-utils';
1111
import { portFinder } from '@studio/common/lib/port-finder';
1212
import { normalizeLineEndings } from '@studio/common/lib/remove-default-db-constants';
1313
import { getServerFilesPath } from '@studio/common/lib/well-known-paths';
14-
import { Blueprint, BlueprintV1Declaration, StepDefinition } from '@wp-playground/blueprints';
14+
import { Blueprint, BlueprintV1Declaration } from '@wp-playground/blueprints';
1515
import { vi, type MockInstance } from 'vitest';
1616
import {
1717
lockCliConfig,
@@ -302,20 +302,8 @@ describe( 'CLI: studio site create', () => {
302302
] ),
303303
} )
304304
);
305-
expect( startWordPressServer ).toHaveBeenCalledWith(
306-
expect.anything(),
307-
expect.any( Logger ),
308-
expect.objectContaining( {
309-
blueprint: expect.objectContaining( {
310-
steps: expect.arrayContaining( [
311-
expect.objectContaining( {
312-
step: 'setSiteOptions',
313-
options: { blogname: 'My Custom Site' },
314-
} ),
315-
] ),
316-
} ),
317-
} )
318-
);
305+
// blogname is now set by playground-server-child via buildSetupSteps, not create.ts
306+
expect( startWordPressServer ).toHaveBeenCalled();
319307
} );
320308

321309
it( 'should NOT override blogname when adding existing WordPress directory with wp-config.php and name', async () => {
@@ -365,21 +353,8 @@ describe( 'CLI: studio site create', () => {
365353
name: 'My Custom Site',
366354
} );
367355

368-
// Verify setSiteOptions step IS in the blueprint steps (because wp-config.php doesn't exist)
369-
expect( startWordPressServer ).toHaveBeenCalledWith(
370-
expect.anything(),
371-
expect.any( Logger ),
372-
expect.objectContaining( {
373-
blueprint: expect.objectContaining( {
374-
steps: expect.arrayContaining( [
375-
expect.objectContaining( {
376-
step: 'setSiteOptions',
377-
options: { blogname: 'My Custom Site' },
378-
} ),
379-
] ),
380-
} ),
381-
} )
382-
);
356+
// blogname is now set by playground-server-child via buildSetupSteps, not create.ts
357+
expect( startWordPressServer ).toHaveBeenCalled();
383358
} );
384359

385360
it( 'should use folder name as site name if no name provided', async () => {
@@ -523,7 +498,7 @@ describe( 'CLI: studio site create', () => {
523498
);
524499
} );
525500

526-
it( 'should prepend setSiteOptions step when name is provided with Blueprint', async () => {
501+
it( 'should pass Blueprint through when name is provided with Blueprint', async () => {
527502
await runCommand( mockSitePath, {
528503
...defaultTestOptions,
529504
name: 'My Site',
@@ -533,18 +508,12 @@ describe( 'CLI: studio site create', () => {
533508
},
534509
} );
535510

511+
// blogname is now set by playground-server-child via buildSetupSteps, not prepended here
536512
expect( startWordPressServer ).toHaveBeenCalledWith(
537513
expect.anything(),
538514
expect.any( Logger ),
539515
expect.objectContaining( {
540-
blueprint: expect.objectContaining( {
541-
steps: expect.arrayContaining( [
542-
expect.objectContaining( {
543-
step: 'setSiteOptions',
544-
options: { blogname: 'My Site' },
545-
} ),
546-
] ),
547-
} ),
516+
blueprint: expect.any( Object ),
548517
} )
549518
);
550519
} );
@@ -661,60 +630,37 @@ describe( 'CLI: studio site create', () => {
661630
expect( disconnectFromDaemon ).toHaveBeenCalled();
662631
} );
663632

664-
it( 'should run Blueprint when preferred language is configured but no Blueprint was given', async () => {
633+
it( 'should create site with siteLanguage when preferred language is configured but no Blueprint given', async () => {
665634
vi.mocked( getPreferredSiteLanguage ).mockResolvedValue( 'es_ES' );
666635

667636
await runCommand( mockSitePath, {
668637
...defaultTestOptions,
669638
noStart: true,
670639
} );
671640

672-
expect( connectToDaemon ).toHaveBeenCalled();
673-
expect( runBlueprint ).toHaveBeenCalledWith(
674-
expect.any( Object ),
675-
expect.any( Object ),
676-
expect.objectContaining( {
677-
blueprint: expect.any( Object ),
678-
blueprintUri: expect.any( String ),
679-
} )
680-
);
641+
// No blueprint to run — language steps are applied by playground-server-child on first start
642+
expect( connectToDaemon ).not.toHaveBeenCalled();
643+
expect( runBlueprint ).not.toHaveBeenCalled();
681644
expect( startWordPressServer ).not.toHaveBeenCalled();
682645
expect( consoleLogSpy ).toHaveBeenCalledWith( 'Site created successfully' );
683-
expect( disconnectFromDaemon ).toHaveBeenCalled();
684646
} );
685647
} );
686648

687649
describe( 'Language Packs', () => {
688-
it( 'should use bundled language packs and skip setSiteLanguage for latest WP version', async () => {
650+
it( 'should use bundled language packs and pass siteLanguage for latest WP version', async () => {
689651
vi.mocked( getPreferredSiteLanguage ).mockResolvedValue( 'sv_SE' );
690652
vi.mocked( copyLanguagePackToSite ).mockResolvedValue( true );
691653

692654
await runCommand( mockSitePath, { ...defaultTestOptions } );
693655

694656
expect( copyLanguagePackToSite ).toHaveBeenCalledWith( mockSitePath, 'sv_SE' );
657+
// Language steps (defineWpConfigConsts / setSiteLanguage) are now built by
658+
// playground-server-child's buildSetupSteps, not by create.ts
695659
expect( startWordPressServer ).toHaveBeenCalledWith(
696660
expect.anything(),
697661
expect.any( Logger ),
698-
expect.objectContaining( {
699-
blueprint: expect.objectContaining( {
700-
steps: expect.arrayContaining( [
701-
expect.objectContaining( {
702-
step: 'defineWpConfigConsts',
703-
consts: { WPLANG: 'sv_SE' },
704-
} ),
705-
expect.objectContaining( {
706-
step: 'setSiteOptions',
707-
options: { WPLANG: 'sv_SE' },
708-
} ),
709-
] ),
710-
} ),
711-
} )
662+
expect.objectContaining( { siteLanguage: 'sv_SE' } )
712663
);
713-
// Should NOT include setSiteLanguage step
714-
const calls = vi.mocked( startWordPressServer ).mock.calls;
715-
const blueprintSteps = ( calls[ 0 ][ 2 ] as { blueprint?: BlueprintV1Declaration } )
716-
?.blueprint?.steps as StepDefinition[];
717-
expect( blueprintSteps.some( ( s ) => s.step === 'setSiteLanguage' ) ).toBe( false );
718664
} );
719665

720666
it( 'should fall back to setSiteLanguage when bundled packs are not available', async () => {
@@ -723,27 +669,15 @@ describe( 'CLI: studio site create', () => {
723669

724670
await runCommand( mockSitePath, { ...defaultTestOptions } );
725671

672+
// setSiteLanguage vs defineWpConfigConsts is now decided by playground-server-child
726673
expect( startWordPressServer ).toHaveBeenCalledWith(
727674
expect.anything(),
728675
expect.any( Logger ),
729-
expect.objectContaining( {
730-
blueprint: expect.objectContaining( {
731-
steps: expect.arrayContaining( [
732-
expect.objectContaining( {
733-
step: 'setSiteLanguage',
734-
language: 'sv_SE',
735-
} ),
736-
expect.objectContaining( {
737-
step: 'setSiteOptions',
738-
options: { WPLANG: 'sv_SE' },
739-
} ),
740-
] ),
741-
} ),
742-
} )
676+
expect.objectContaining( { siteLanguage: 'sv_SE' } )
743677
);
744678
} );
745679

746-
it( 'should use setSiteLanguage for non-latest WP versions', async () => {
680+
it( 'should pass siteLanguage for non-latest WP versions', async () => {
747681
vi.mocked( getPreferredSiteLanguage ).mockResolvedValue( 'sv_SE' );
748682

749683
await runCommand( mockSitePath, {
@@ -752,19 +686,11 @@ describe( 'CLI: studio site create', () => {
752686
} );
753687

754688
expect( copyLanguagePackToSite ).not.toHaveBeenCalled();
689+
// setSiteLanguage step is now built by playground-server-child, not create.ts
755690
expect( startWordPressServer ).toHaveBeenCalledWith(
756691
expect.anything(),
757692
expect.any( Logger ),
758-
expect.objectContaining( {
759-
blueprint: expect.objectContaining( {
760-
steps: expect.arrayContaining( [
761-
expect.objectContaining( {
762-
step: 'setSiteLanguage',
763-
language: 'sv_SE',
764-
} ),
765-
] ),
766-
} ),
767-
} )
693+
expect.objectContaining( { siteLanguage: 'sv_SE' } )
768694
);
769695
} );
770696

apps/cli/lib/daemon-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ const daemonStartProcessSuccessResponseSchema = z.object( {
307307
export async function startProcess(
308308
processName: string,
309309
scriptPath: string,
310-
env: Record< string, string > = {},
310+
env: NodeJS.ProcessEnv = process.env,
311311
args: string[] = []
312312
): Promise< ProcessDescription > {
313313
const response = await sendDaemonRequest( {

apps/cli/lib/dependency-management/paths.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ export function getAiInstructionsPath(): string {
5757
export function getPhpMyAdminPath(): string {
5858
return path.join( getWpFilesPath(), 'phpmyadmin' );
5959
}
60+
61+
export function getBlueprintsPharPath(): string {
62+
return path.join( getWpFilesPath(), 'blueprints', 'blueprints.phar' );
63+
}

apps/cli/lib/types/process-manager-ipc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const daemonRequestStartProcessSchema = z.object( {
3131
type: z.literal( 'start-process' ),
3232
processName: z.string(),
3333
scriptPath: z.string(),
34-
env: z.record( z.string(), z.string() ).optional(),
34+
env: z.record( z.string(), z.union( [ z.string(), z.undefined() ] ) ).optional(),
3535
args: z.array( z.string() ).optional(),
3636
} );
3737

0 commit comments

Comments
 (0)