diff --git a/cli/commands/site/list.ts b/cli/commands/site/list.ts index e4db8b48a1..196c26c3df 100644 --- a/cli/commands/site/list.ts +++ b/cli/commands/site/list.ts @@ -126,6 +126,8 @@ export async function runCommand( format: 'table' | 'json', watch: boolean ): Pr }, { debounceMs: 500 } ); + + process.on( 'SIGINT', disconnect ); } } finally { if ( ! watch ) { diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index 221910e44a..46d1cccf90 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -18,7 +18,7 @@ test.describe( 'Blueprints', () => { await onboarding.closeWhatsNew(); const siteContent = new SiteContent( session.mainWindow, DEFAULT_SITE_NAME ); - await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } ); + await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 200_000 } ); } ); test.afterAll( async () => { @@ -49,7 +49,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -85,7 +85,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -123,7 +123,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -159,7 +159,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -197,7 +197,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to verify site is accessible const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -236,7 +236,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to verify site is accessible const settingsTab = await siteContent.navigateToTab( 'Settings' ); diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index dcd4bbdedf..4b7795bf3d 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -4,6 +4,7 @@ import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; +import { rimraf } from 'rimraf'; export class E2ESession { electronApp: ElectronApplication; @@ -92,7 +93,8 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); + // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { recursive: true, force: true } ); + await rimraf( this.sessionPath ); } } diff --git a/e2e/fixtures/blueprints/activate-plugin.json b/e2e/fixtures/blueprints/activate-plugin.json index 6b4f6b326c..111bd4eefc 100644 --- a/e2e/fixtures/blueprints/activate-plugin.json +++ b/e2e/fixtures/blueprints/activate-plugin.json @@ -4,7 +4,7 @@ "steps": [ { "step": "installPlugin", - "pluginZipFile": { + "pluginData": { "resource": "wordpress.org/plugins", "slug": "hello-dolly" } @@ -14,4 +14,4 @@ "pluginPath": "hello-dolly/hello.php" } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/activate-theme.json b/e2e/fixtures/blueprints/activate-theme.json index fe60126e93..2600eab205 100644 --- a/e2e/fixtures/blueprints/activate-theme.json +++ b/e2e/fixtures/blueprints/activate-theme.json @@ -4,7 +4,7 @@ "steps": [ { "step": "installTheme", - "themeZipFile": { + "themeData": { "resource": "wordpress.org/themes", "slug": "twentytwentyone" } @@ -14,4 +14,4 @@ "themeFolderName": "twentytwentyone" } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/install-plugin.json b/e2e/fixtures/blueprints/install-plugin.json index 76c840c7f5..2dd6e971bf 100644 --- a/e2e/fixtures/blueprints/install-plugin.json +++ b/e2e/fixtures/blueprints/install-plugin.json @@ -4,10 +4,10 @@ "steps": [ { "step": "installPlugin", - "pluginZipFile": { + "pluginData": { "resource": "wordpress.org/plugins", "slug": "akismet" } } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/install-theme.json b/e2e/fixtures/blueprints/install-theme.json index 5bb3492410..d718e55e37 100644 --- a/e2e/fixtures/blueprints/install-theme.json +++ b/e2e/fixtures/blueprints/install-theme.json @@ -4,10 +4,10 @@ "steps": [ { "step": "installTheme", - "themeZipFile": { + "themeData": { "resource": "wordpress.org/themes", "slug": "twentytwentytwo" } } ] -} \ No newline at end of file +} diff --git a/e2e/localization.test.ts b/e2e/localization.test.ts index cab3c30e63..572452977f 100644 --- a/e2e/localization.test.ts +++ b/e2e/localization.test.ts @@ -139,7 +139,7 @@ test.describe( 'Localization', () => { // Wait for site to be created const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 200_000 } ); const settingsTabButton = session.mainWindow.getByRole( 'tab', { name: /Settings|設定/i } ); await settingsTabButton.click(); diff --git a/package-lock.json b/package-lock.json index 70146c823e..2c2f048a10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", + "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", @@ -5346,6 +5347,29 @@ } } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6427,6 +6451,23 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@octokit/app": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", @@ -13414,6 +13455,69 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/cacache/node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -23602,10 +23706,11 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "1.0.11", @@ -25408,15 +25513,91 @@ "license": "MIT" }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" diff --git a/package.json b/package.json index 3a9a28cbdb..53ea5969a1 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", + "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", diff --git a/src/index.ts b/src/index.ts index 09892facd7..c05f073b3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -311,9 +311,11 @@ async function appBoot() { await renameLaunchUniquesStat(); - await createMainWindow(); await startUserDataWatcher(); - startSiteWatcher(); + + await startSiteWatcher(); + + await createMainWindow(); const userData = await loadUserData(); // Bump stats for the first time the app runs - this is when no lastBumpStats are available diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 0cdb0d01df..4ca81b8cda 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -10,8 +10,9 @@ export interface CliCommandResult { } type CliCommandEventMap = { - data: { data: unknown }; + started: void; error: { error: Error }; + data: { data: unknown }; success: { result?: CliCommandResult }; failure: { result?: CliCommandResult }; }; @@ -40,11 +41,12 @@ export interface ExecuteCliCommandOptions { * - 'capture': capture stdout/stderr, available in success/failure events */ output: 'ignore' | 'capture'; + detached?: boolean; } export function executeCliCommand( args: string[], - options: ExecuteCliCommandOptions = { output: 'ignore' } + options: ExecuteCliCommandOptions = { output: 'ignore', detached: false } ): [ CliCommandEventEmitter, ChildProcess ] { const cliPath = getCliPath(); @@ -58,6 +60,7 @@ export function executeCliCommand( // Using Electron's utilityProcess.fork API gave us issues with the child process never exiting const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, + detached: options.detached, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', @@ -65,6 +68,16 @@ export function executeCliCommand( } ); const eventEmitter = new CliCommandEventEmitter(); + child.on( 'spawn', () => { + eventEmitter.emit( 'started' ); + } ); + + child.on( 'error', ( error ) => { + console.error( 'Child process error:', error ); + Sentry.captureException( error ); + eventEmitter.emit( 'error', { error } ); + } ); + let stdout = ''; let stderr = ''; @@ -81,12 +94,6 @@ export function executeCliCommand( eventEmitter.emit( 'data', { data: message } ); } ); - child.on( 'error', ( error ) => { - console.error( 'Child process error:', error ); - Sentry.captureException( error ); - eventEmitter.emit( 'error', { error } ); - } ); - let capturedExitCode: number | null = null; child.on( 'exit', ( code ) => { @@ -107,9 +114,13 @@ export function executeCliCommand( } } ); - process.on( 'exit', () => { - child.kill(); - } ); + if ( options.detached ) { + child.unref(); + } else { + process.on( 'exit', () => { + child.kill(); + } ); + } return [ eventEmitter, child ]; } diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 996643b620..b99b2a475c 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -71,37 +71,44 @@ async function updateSiteServerStatus( await current; } -export function startSiteWatcher(): void { - if ( watcher ) { - return; - } +export async function startSiteWatcher(): Promise< void > { + return new Promise( ( resolve, reject ) => { + if ( watcher ) { + return resolve(); + } - watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { - output: 'ignore', - } ); - const [ eventEmitter ] = watcher; + watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { + output: 'ignore', + } ); + const [ eventEmitter ] = watcher; - eventEmitter.on( 'data', ( { data } ) => { - const parsed = siteStatusEventSchema.safeParse( data ); - if ( ! parsed.success ) { - return; - } + eventEmitter.on( 'started', () => { + resolve(); + } ); - const { siteId, status, url } = parsed.data.value; - const isRunning = status === 'running'; + eventEmitter.on( 'error', ( { error } ) => { + reject(); + console.error( 'Site watcher error:', error ); + watcher = null; + } ); - void updateSiteServerStatus( siteId, isRunning, url ); - void sendIpcEventToRenderer( 'site-status-changed', parsed.data.value ); - } ); + eventEmitter.on( 'data', ( { data } ) => { + const parsed = siteStatusEventSchema.safeParse( data ); + if ( ! parsed.success ) { + return; + } - eventEmitter.on( 'error', ( { error } ) => { - console.error( 'Site watcher error:', error ); - watcher = null; - } ); + const { siteId, status, url } = parsed.data.value; + const isRunning = status === 'running'; - eventEmitter.on( 'failure', () => { - console.warn( 'Site watcher exited unexpectedly' ); - watcher = null; + void updateSiteServerStatus( siteId, isRunning, url ); + void sendIpcEventToRenderer( 'site-status-changed', parsed.data.value ); + } ); + + eventEmitter.on( 'failure', () => { + console.warn( 'Site watcher exited unexpectedly' ); + watcher = null; + } ); } ); } diff --git a/src/site-server.ts b/src/site-server.ts index 18795213d4..63c69d9edb 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -43,6 +43,7 @@ export async function stopAllServersOnQuit() { return new Promise< void >( ( resolve ) => { const [ emitter ] = executeCliCommand( [ 'site', 'stop-all', '--auto-start' ], { output: 'ignore', + detached: true, } ); emitter.on( 'success', () => resolve() ); emitter.on( 'failure', () => resolve() );