Skip to content

Commit 2a2328e

Browse files
authored
Merge branch 'trunk' into stu-830-add-support-for-blueprints-definesiteurl
2 parents d076231 + af4974d commit 2a2328e

File tree

13 files changed

+151
-40
lines changed

13 files changed

+151
-40
lines changed

RELEASE-NOTES.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Unreleased
22
==========
33

4+
1.7.3
5+
=====
6+
* Added Zed and Antigravity editor support #2497
7+
* Fixed incorrect WordPress version warning when applying Blueprints #2494
8+
* Fixed an issue where deleting multiple sites at once could fail #2492
9+
* Fixed tab selection after site deletion #2468
10+
* Fixed sites failing to start when Studio is installed in a path containing spaces #2495
11+
* CLI: Fixed --skip-site-details argument for starting a site that is already running #2510
12+
* Updated multiple dependencies, including WordPress Element, WordPress Date, and Node.js types
13+
414
1.7.2
515
=====
616
* Added pause/resume functionality for the push process #2379

cli/commands/site/create.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import {
1515
validateBlueprintData,
1616
} from 'common/lib/blueprint-validation';
1717
import { getDomainNameValidationError } from 'common/lib/domains';
18-
import { arePathsEqual, isEmptyDir, isWordPressDirectory, pathExists } from 'common/lib/fs-utils';
18+
import {
19+
arePathsEqual,
20+
isEmptyDir,
21+
isWordPressDirectory,
22+
pathExists,
23+
recursiveCopyDirectory,
24+
} from 'common/lib/fs-utils';
1925
import { DEFAULT_LOCALE } from 'common/lib/locale';
2026
import { isOnline } from 'common/lib/network-utils';
2127
import { createPassword } from 'common/lib/passwords';
@@ -27,7 +33,6 @@ import {
2733
isWordPressVersionAtLeast,
2834
} from 'common/lib/wordpress-version-utils';
2935
import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
30-
import fse from 'fs-extra';
3136
import {
3237
lockAppdata,
3338
readAppdata,
@@ -55,6 +60,7 @@ const logger = new Logger< LoggerAction >();
5560

5661
type CreateCommandOptions = {
5762
name?: string;
63+
siteId?: string;
5864
wpVersion: string;
5965
phpVersion: ( typeof ALLOWED_PHP_VERSIONS )[ number ];
6066
customDomain?: string;
@@ -156,7 +162,7 @@ export async function runCommand(
156162
}
157163

158164
logger.reportStart( LoggerAction.SETUP_WORDPRESS, __( 'Copying bundled WordPress…' ) );
159-
await fse.copy( bundledWPPath, sitePath );
165+
await recursiveCopyDirectory( bundledWPPath, sitePath );
160166
logger.reportSuccess( __( 'WordPress files copied' ) );
161167
} else if ( ! isOnlineStatus ) {
162168
throw new LoggerError(
@@ -178,7 +184,7 @@ export async function runCommand(
178184
logger.reportSuccess( sprintf( __( 'Port assigned: %d' ), port ) );
179185

180186
const siteName = options.name || path.basename( sitePath );
181-
const siteId = crypto.randomUUID();
187+
const siteId = options.siteId || crypto.randomUUID();
182188
const adminPassword = createPassword();
183189

184190
const setupSteps: StepDefinition[] = [];
@@ -364,6 +370,15 @@ function readBlueprint( blueprintPath: string ) {
364370
}
365371
}
366372

373+
function coerceSiteId( value: string ) {
374+
// Validate UUID format (8-4-4-4-12 hex characters)
375+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
376+
if ( ! uuidRegex.test( value ) ) {
377+
throw new ValidationError( 'id', value, __( 'Must be a valid UUID' ) );
378+
}
379+
return value;
380+
}
381+
367382
function coerceWpVersion( value: string ) {
368383
if ( ! isValidWordPressVersion( value ) ) {
369384
throw new ValidationError(
@@ -392,6 +407,12 @@ export const registerCommand = ( yargs: StudioArgv ) => {
392407
describe: __( 'Create a new site' ),
393408
builder: ( yargs ) => {
394409
return yargs
410+
.option( 'id', {
411+
type: 'string',
412+
describe: __( 'Site ID (UUID format, used internally by Studio app)' ),
413+
hidden: true,
414+
coerce: coerceSiteId,
415+
} )
395416
.option( 'name', {
396417
type: 'string',
397418
describe: __( 'Site name' ),
@@ -440,6 +461,7 @@ export const registerCommand = ( yargs: StudioArgv ) => {
440461
handler: async ( argv ) => {
441462
const config: CreateCommandOptions = {
442463
name: argv.name,
464+
siteId: argv.id,
443465
wpVersion: argv.wp,
444466
phpVersion: argv.php,
445467
customDomain: argv.domain,

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import {
44
filterUnsupportedBlueprintFeatures,
55
validateBlueprintData,
66
} from 'common/lib/blueprint-validation';
7-
import { isEmptyDir, isWordPressDirectory, pathExists, arePathsEqual } from 'common/lib/fs-utils';
7+
import {
8+
isEmptyDir,
9+
isWordPressDirectory,
10+
pathExists,
11+
arePathsEqual,
12+
recursiveCopyDirectory,
13+
} from 'common/lib/fs-utils';
814
import { isOnline } from 'common/lib/network-utils';
915
import { portFinder } from 'common/lib/port-finder';
1016
import { vi, type MockInstance } from 'vitest';
@@ -28,11 +34,6 @@ import { runCommand } from '../create';
2834

2935
vi.mock( 'common/lib/fs-utils' );
3036
vi.mock( 'common/lib/network-utils' );
31-
vi.mock( 'fs-extra', () => ( {
32-
default: {
33-
copy: vi.fn().mockResolvedValue( undefined ),
34-
},
35-
} ) );
3637
vi.mock( 'common/lib/port-finder', () => ( {
3738
portFinder: {
3839
addUnavailablePort: vi.fn(),
@@ -133,6 +134,7 @@ describe( 'CLI: studio site create', () => {
133134
vi.mocked( isEmptyDir ).mockResolvedValue( true );
134135
vi.mocked( isWordPressDirectory ).mockReturnValue( false );
135136
vi.mocked( arePathsEqual ).mockImplementation( ( a, b ) => a === b );
137+
vi.mocked( recursiveCopyDirectory ).mockResolvedValue( undefined );
136138
vi.mocked( portFinder.getOpenPort ).mockResolvedValue( mockPort );
137139
vi.mocked( readAppdata, { partial: true } ).mockResolvedValue( {
138140
sites: [ ...mockAppdata.sites ],

common/lib/fs-utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ export async function pathExists( path: string ): Promise< boolean > {
7777
}
7878
}
7979

80+
export async function recursiveCopyDirectory(
81+
source: string,
82+
destination: string
83+
): Promise< void > {
84+
await fsPromises.mkdir( destination, { recursive: true } );
85+
86+
const entries = await fsPromises.readdir( source, { withFileTypes: true } );
87+
88+
for ( const entry of entries ) {
89+
const sourcePath = path.join( source, entry.name );
90+
const destinationPath = path.join( destination, entry.name );
91+
92+
if ( entry.isDirectory() ) {
93+
await recursiveCopyDirectory( sourcePath, destinationPath );
94+
} else if ( entry.isFile() ) {
95+
await fsPromises.copyFile( sourcePath, destinationPath );
96+
}
97+
}
98+
}
99+
80100
export async function isEmptyDir( directory: string ): Promise< boolean > {
81101
const stats = await fsPromises.stat( directory );
82102
if ( ! stats.isDirectory() ) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"author": "Automattic Inc.",
44
"private": true,
55
"productName": "Studio",
6-
"version": "1.7.3-beta1",
6+
"version": "1.7.3",
77
"description": "Local WordPress development environment using Playgrounds",
88
"license": "GPL-2.0-or-later",
99
"main": "dist/main/index.js",

src/ipc-handlers.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import fs from 'fs';
1515
import fsPromises from 'fs/promises';
1616
import https from 'node:https';
17+
import os from 'os';
1718
import nodePath from 'path';
1819
import * as Sentry from '@sentry/electron/main';
1920
import { __, sprintf, LocaleData, defaultI18n } from '@wordpress/i18n';
@@ -122,6 +123,38 @@ export {
122123
showUserSettings,
123124
} from 'src/modules/user-settings/lib/ipc-handlers';
124125

126+
const DEBUG_LOG_MAX_LINES = 50;
127+
const PM2_HOME = nodePath.join( os.homedir(), '.studio', 'pm2' );
128+
129+
function readLastLines( filePath: string, maxLines: number ): string[] | undefined {
130+
try {
131+
if ( ! fs.existsSync( filePath ) ) {
132+
return undefined;
133+
}
134+
const content = fs.readFileSync( filePath, 'utf-8' );
135+
const lines = content.split( '\n' ).filter( ( line ) => line.trim() );
136+
return lines.slice( -maxLines );
137+
} catch {
138+
return undefined;
139+
}
140+
}
141+
142+
function readWordPressDebugLog( sitePath: string ): string[] | undefined {
143+
const debugLogPath = nodePath.join( sitePath, 'wp-content', 'debug.log' );
144+
return readLastLines( debugLogPath, DEBUG_LOG_MAX_LINES );
145+
}
146+
147+
function readPm2Logs( siteId: string ): { stdout?: string[]; stderr?: string[] } {
148+
const logsDir = nodePath.join( PM2_HOME, 'logs' );
149+
const stdoutPath = nodePath.join( logsDir, `studio-site-${ siteId }-out.log` );
150+
const stderrPath = nodePath.join( logsDir, `studio-site-${ siteId }-error.log` );
151+
152+
return {
153+
stdout: readLastLines( stdoutPath, DEBUG_LOG_MAX_LINES ),
154+
stderr: readLastLines( stderrPath, DEBUG_LOG_MAX_LINES ),
155+
};
156+
}
157+
125158
function mergeSiteDetailsWithRunningDetails( sites: SiteDetails[] ): SiteDetails[] {
126159
return sites.map( ( site ) => {
127160
const server = SiteServer.get( site.id );
@@ -221,12 +254,14 @@ export async function createSite(
221254
wpVersion,
222255
customDomain,
223256
enableHttps,
224-
siteId,
257+
siteId: providedSiteId,
225258
blueprint,
226259
phpVersion,
227260
noStart = false,
228261
} = config;
229262

263+
const siteId = providedSiteId || crypto.randomUUID();
264+
230265
const metric = getBlueprintMetric( blueprint?.slug );
231266
bumpStat( StatsGroup.STUDIO_SITE_CREATE, metric );
232267

@@ -273,6 +308,19 @@ export async function createSite(
273308
contexts.startup = cliError.cliArgs;
274309
}
275310

311+
const debugLog = readWordPressDebugLog( path );
312+
if ( debugLog && debugLog.length > 0 ) {
313+
contexts.debugLog = { entries: debugLog };
314+
}
315+
316+
const pm2Logs = readPm2Logs( siteId );
317+
if ( pm2Logs.stdout && pm2Logs.stdout.length > 0 ) {
318+
contexts.playgroundLogs = { entries: pm2Logs.stdout };
319+
}
320+
if ( pm2Logs.stderr && pm2Logs.stderr.length > 0 ) {
321+
contexts.playgroundErrors = { entries: pm2Logs.stderr };
322+
}
323+
276324
Sentry.captureException( error, {
277325
tags: {
278326
provider: 'cli',
@@ -388,6 +436,19 @@ export async function startServer( event: IpcMainInvokeEvent, id: string ): Prom
388436
contexts.startup = cliError.cliArgs;
389437
}
390438

439+
const debugLog = readWordPressDebugLog( server.details.path );
440+
if ( debugLog && debugLog.length > 0 ) {
441+
contexts.debugLog = { entries: debugLog };
442+
}
443+
444+
const pm2Logs = readPm2Logs( id );
445+
if ( pm2Logs.stdout && pm2Logs.stdout.length > 0 ) {
446+
contexts.playgroundLogs = { entries: pm2Logs.stdout };
447+
}
448+
if ( pm2Logs.stderr && pm2Logs.stderr.length > 0 ) {
449+
contexts.playgroundErrors = { entries: pm2Logs.stderr };
450+
}
451+
391452
Sentry.captureException( error, {
392453
tags: {
393454
provider: 'cli',

src/lib/wordpress-setup.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
*/
55

66
import nodePath from 'path';
7-
import fs from 'fs-extra';
8-
import { pathExists } from 'common/lib/fs-utils';
7+
import { pathExists, recursiveCopyDirectory } from 'common/lib/fs-utils';
98
import { getResourcesPath } from 'src/storage/paths';
109

1110
/**
@@ -19,5 +18,5 @@ export async function setupWordPressFilesOnly( path: string ): Promise< void > {
1918
throw new Error( 'Bundled WordPress files not found. Please reinstall WordPress Studio.' );
2019
}
2120

22-
await fs.copy( bundledWPPath, path );
21+
await recursiveCopyDirectory( bundledWPPath, path );
2322
}

src/lib/wp-versions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'path';
22
import fs from 'fs-extra';
33
import semver from 'semver';
4+
import { recursiveCopyDirectory } from 'common/lib/fs-utils';
45
import { downloadWordPress } from 'src/lib/download-utils';
56
import { getWordPressVersionPath } from 'src/lib/server-files-paths';
67

@@ -67,7 +68,10 @@ export async function updateLatestWordPressVersion() {
6768
const latestVersion = await getLatestWordPressVersion();
6869
if ( installedVersion && latestVersion !== 'latest' && installedVersion !== latestVersion ) {
6970
// We keep a copy of the latest installed version instead of removing it.
70-
await fs.copy( latestVersionPath, getWordPressVersionPath( installedVersion ) );
71+
await recursiveCopyDirectory(
72+
latestVersionPath,
73+
getWordPressVersionPath( installedVersion )
74+
);
7175
shouldOverwrite = true;
7276
}
7377
}

src/migrations/migrate-from-wp-now-folder.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { app } from 'electron';
22
import path from 'path';
3-
import fs from 'fs-extra';
4-
import { pathExists } from 'common/lib/fs-utils';
3+
import { pathExists, recursiveCopyDirectory } from 'common/lib/fs-utils';
54
import { getServerFilesPath } from 'src/storage/paths';
65
import { loadUserData } from 'src/storage/user-data';
76

@@ -27,5 +26,5 @@ export async function needsToMigrateFromWpNowFolder() {
2726
}
2827

2928
export async function migrateFromWpNowFolder() {
30-
await fs.copy( wpNowPath, getServerFilesPath() );
29+
await recursiveCopyDirectory( wpNowPath, getServerFilesPath() );
3130
}

0 commit comments

Comments
 (0)