Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ async function main() {
.strict();

await studioArgv.argv;

// Tell the host that started this command it has finished, so it needn't wait for the
// process to exit after best-effort telemetry requests complete.
if ( process.connected && ! process.exitCode ) {
process.send?.( { action: 'done' } );
}
}

void main();
38 changes: 27 additions & 11 deletions packages/common/lib/cli-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ const cliErrorMessageSchema = z.object( {
message: z.string(),
} );

/**
* Sent by the CLI once a command has finished successfully. Its process can stay alive a while
* longer to finish sending best-effort telemetry, which its host doesn't need to wait for.
*/
const cliDoneMessageSchema = z.object( { action: z.literal( 'done' ) } );

export function isCliCommandDone( message: unknown ): boolean {
return cliDoneMessageSchema.safeParse( message ).success;
}

type CliCommandEventEmitter< CapturesOutput extends boolean > = TypedEventEmitter<
CliCommandEventMap< CapturesOutput >
>;
Expand Down Expand Up @@ -327,7 +337,8 @@ const cliProgressMessageSchema = z.object( {

/**
* Runs a CLI command and resolves with the result it reports (`logger.reportResult`), parsed by
* `schema`. Rejects with the command's error, or when it succeeds without reporting a result.
* `schema`, once the command has finished. Rejects with the command's error, or when it succeeds
* without reporting a result.
*/
export function runCliCommand< T >(
execute: ExecuteCliCommand,
Expand All @@ -338,27 +349,32 @@ export function runCliCommand< T >(
return new Promise( ( resolve, reject ) => {
let result: z.ZodSafeParseResult< T > | undefined;
const [ emitter ] = execute( args, { output: 'capture', logPrefix: options.logPrefix } );
const settle = () => {
if ( result?.success ) {
resolve( result.data );
} else {
reject(
result?.error ??
new Error( `\`studio ${ args.slice( 0, 2 ).join( ' ' ) }\` reported no result.` )
);
}
};
emitter.on( 'data', ( { data } ) => {
const message = cliResultMessageSchema.safeParse( data );
if ( message.success ) {
result = schema.safeParse( message.data.value );
return;
}
if ( isCliCommandDone( data ) ) {
settle();
return;
}
const progress = cliProgressMessageSchema.safeParse( data );
if ( progress.success ) {
options.onProgress?.( progress.data.message );
}
} );
emitter.on( 'success', () => {
if ( result?.success ) {
resolve( result.data );
} else {
reject(
result?.error ??
new Error( `\`studio ${ args.slice( 0, 2 ).join( ' ' ) }\` reported no result.` )
);
}
} );
emitter.on( 'success', settle );
emitter.on( 'failure', ( { error } ) => reject( error ) );
emitter.on( 'error', ( { error } ) => reject( error ) );
} );
Expand Down
15 changes: 15 additions & 0 deletions packages/common/lib/tests/cli-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ describe( 'runCliCommand', () => {
);
} );

it( 'resolves once the command says it finished, while its process is still running', async () => {
const emitter = new EventEmitter();
const execute = vi.fn( () => {
queueMicrotask( () => {
emitter.emit( 'data', { data: { action: 'result', value: { url: 'https://a.example' } } } );
emitter.emit( 'data', { data: { action: 'done' } } );
} );
return [ emitter, {} ];
} ) as unknown as ExecuteCliCommand;

await expect( runCliCommand( execute, [ 'site', 'create' ], schema ) ).resolves.toEqual( {
url: 'https://a.example',
} );
} );

it( 'rejects with the command error when it fails', async () => {
const emitter = new EventEmitter();
const execute = vi.fn( () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/common/sites/snapshots.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import crypto from 'node:crypto';
import { z } from 'zod';
import { runCliCommand, type ExecuteCliCommand } from '@studio/common/lib/cli-process';
import {
isCliCommandDone,
runCliCommand,
type ExecuteCliCommand,
} from '@studio/common/lib/cli-process';
import { PreviewCommandLoggerAction } from '@studio/common/logger-actions';
import { snapshotSchema, type Snapshot } from '@studio/common/types/snapshot';

Expand Down Expand Up @@ -60,6 +64,9 @@ export function createSnapshotManager( ctx: SnapshotCommandContext ): SnapshotMa
const [ emitter ] = ctx.executeCliCommand( args, { output: 'capture', logPrefix: 'preview' } );

emitter.on( 'data', ( { data } ) => {
if ( isCliCommandDone( data ) ) {
return;
}
const parsed = snapshotEventSchema.safeParse( data );
if ( ! parsed.success ) {
console.error( 'Invalid snapshot event:', parsed.error );
Expand Down
Loading