Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions apps/studio/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ function launchExtensionBackgroundWorkers( appSession = session.defaultSession )
);
}

async function setupDevelopmentExtensions(): Promise< void > {
try {
await installExtension( REACT_DEVELOPER_TOOLS );
await installExtension( REDUX_DEVTOOLS );
await launchExtensionBackgroundWorkers();
} catch ( error ) {
console.warn( 'Failed to initialize development extensions:', error );
}
}

async function appBoot() {
app.setName( packageJson.productName );

Expand Down Expand Up @@ -263,9 +273,7 @@ async function appBoot() {
app.on( 'ready', async () => {
const locale = await getUserLocaleWithFallback();
if ( process.env.NODE_ENV === 'development' ) {
await installExtension( REACT_DEVELOPER_TOOLS );
await installExtension( REDUX_DEVTOOLS );
await launchExtensionBackgroundWorkers();
await setupDevelopmentExtensions();
}

console.log( `App version: ${ app.getVersion() }` );
Expand Down
20 changes: 8 additions & 12 deletions apps/studio/src/modules/cli/lib/cli-events-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,22 @@ const handleSiteEvent = sequential( async ( event: SiteEvent ): Promise< void >
return;
}

// Only register new sites on CREATED events to prevent duplicates
let server = SiteServer.get( siteId ) ?? SiteServer.getByPath( site.path );
if ( ! server ) {
// Sites started outside the desktop app are announced as UPDATED events.
// Register them here so startup sync and external CLI actions surface correctly.
server = SiteServer.register( siteDetailsToServerDetails( site, running ) );
}

if ( eventType === SITE_EVENTS.CREATED ) {
const existingServer = SiteServer.get( siteId ) ?? SiteServer.getByPath( site.path );
if ( ! existingServer ) {
SiteServer.register( siteDetailsToServerDetails( site, running ) );
}
// Don't send to renderer if site is being created by UI (createSite IPC will handle it)
if ( existingServer?.hasOngoingOperation ) {
if ( server.hasOngoingOperation ) {
return;
}
void sendIpcEventToRenderer( 'site-event', event );
return;
}

// For UPDATED events, only update if the site already exists
const server = SiteServer.get( siteId ) ?? SiteServer.getByPath( site.path );
if ( ! server ) {
return;
}

server.details = siteDetailsToServerDetails( site, running, server.details );

if ( server.server && site.url ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @vitest-environment node
*/
import { EventEmitter } from 'node:events';
import { SITE_EVENTS } from '@studio/common/lib/site-events';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const sendIpcEventToRenderer = vi.fn();
const executeCliCommand = vi.fn();
const register = vi.fn();
const unregister = vi.fn();
const get = vi.fn();
const getByPath = vi.fn();

let currentEmitter: EventEmitter;
let currentChildProcess: { kill: ReturnType< typeof vi.fn > };

vi.mock( 'src/ipc-utils', () => ( {
sendIpcEventToRenderer,
} ) );

vi.mock( 'src/modules/cli/lib/execute-command', () => ( {
executeCliCommand,
} ) );

vi.mock( 'src/site-server', () => ( {
SiteServer: {
register,
unregister,
get,
getByPath,
},
} ) );

describe( 'cli-events-subscriber', () => {
beforeEach( () => {
vi.resetModules();
vi.clearAllMocks();

currentEmitter = new EventEmitter();
currentChildProcess = { kill: vi.fn() };
executeCliCommand.mockReturnValue( [ currentEmitter, currentChildProcess ] );
get.mockReturnValue( undefined );
getByPath.mockReturnValue( undefined );
register.mockImplementation( ( details ) => ( {
details,
hasOngoingOperation: false,
server: { url: details.url },
} ) );
} );

it( 'registers missing sites from UPDATED events so external CLI starts appear in the app', async () => {
const { startCliEventsSubscriber, stopCliEventsSubscriber } = await import(
'src/modules/cli/lib/cli-events-subscriber'
);

const startPromise = startCliEventsSubscriber();
currentEmitter.emit( 'started' );
await startPromise;

const siteEvent = {
event: SITE_EVENTS.UPDATED,
siteId: 'site-1',
running: true,
site: {
id: 'site-1',
name: 'AI Site',
path: '/sites/ai-site',
port: 8881,
url: 'http://localhost:8881',
phpVersion: '8.3',
},
};

currentEmitter.emit( 'data', {
data: {
action: 'keyValuePair',
key: 'site-event',
value: JSON.stringify( siteEvent ),
},
} );

await vi.waitFor( () => {
expect( register ).toHaveBeenCalledWith( {
...siteEvent.site,
running: true,
} );
} );

expect( sendIpcEventToRenderer ).toHaveBeenCalledWith( 'site-event', siteEvent );

stopCliEventsSubscriber();
expect( currentChildProcess.kill ).toHaveBeenCalledWith( 'SIGKILL' );
} );
} );
44 changes: 44 additions & 0 deletions apps/studio/src/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,50 @@ describe( 'App initialization', () => {
await expect( import( '../index' ) ).resolves.toBeDefined();
} );

it( 'should continue booting when development extension workers fail to start', async () => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const warnSpy = vi.spyOn( console, 'warn' ).mockImplementation( () => {} );

try {
const { mockedEvents } = mockElectron();
vi.resetModules();

const { session } = await import( 'electron' );
const serviceWorkerError = new Error( 'Failed to start service worker.' );
vi.mocked( session.defaultSession.extensions.getAllExtensions ).mockReturnValue( [
{
id: 'test-extension',
manifest: {
manifest_version: 3,
background: {
service_worker: 'background.js',
},
},
name: 'Test Extension',
path: '/mock/extensions/test-extension',
url: 'chrome-extension://test-extension/',
version: '1.0.0',
},
] );
vi.mocked( session.defaultSession.serviceWorkers.startWorkerForScope ).mockRejectedValue(
serviceWorkerError
);

await import( '../index' );
await expect( mockedEvents.ready() ).resolves.toBeUndefined();
expect( warnSpy ).toHaveBeenCalledWith(
'Failed to initialize development extensions:',
serviceWorkerError
);

await mockedEvents[ 'will-quit' ]( { preventDefault: vi.fn() } );
} finally {
process.env.NODE_ENV = originalNodeEnv;
warnSpy.mockRestore();
}
} );

it( 'should handle authentication deep links', async () => {
const originalProcessPlatform = process.platform;
Object.defineProperty( process, 'platform', { value: 'darwin' } );
Expand Down
Loading