@@ -17,6 +17,7 @@ import { z } from 'zod';
17
17
import { editToolParameters } from './editTool' ;
18
18
import { getAbsolutePath } from '~/lib/stores/files' ;
19
19
import { streamOutput } from '~/utils/process' ;
20
+ import { outputLabels , type OutputLabels } from './deployToolOutputLabels' ;
20
21
21
22
const logger = createScopedLogger ( 'ActionRunner' ) ;
22
23
@@ -372,30 +373,94 @@ export class ActionRunner {
372
373
case 'deploy' : {
373
374
const container = await this . #webcontainer;
374
375
await waitForContainerBootState ( ContainerBootState . READY ) ;
375
- const convexProc = await container . spawn ( 'sh' , [
376
- '-c' ,
377
- 'convex dev --once && tsc --noEmit -p tsconfig.app.json' ,
378
- ] ) ;
379
- action . abortSignal . addEventListener ( 'abort' , ( ) => {
380
- convexProc . kill ( ) ;
381
- } ) ;
382
376
383
- const { output, exitCode } = await streamOutput ( convexProc , {
384
- onOutput : ( output ) => {
377
+ result = '' ;
378
+
379
+ const commandErroredController = new AbortController ( ) ;
380
+ const abortSignal = AbortSignal . any ( [ action . abortSignal , commandErroredController . signal ] ) ;
381
+
382
+ /** Return a promise of output on success, throws an error containing output on failure. */
383
+ const run = async (
384
+ commandAndArgs : string [ ] ,
385
+ errorPrefix : OutputLabels ,
386
+ onOutput ?: ( s : string ) => void ,
387
+ ) : Promise < string > => {
388
+ logger . info ( 'starting to run' , errorPrefix ) ;
389
+ const t0 = performance . now ( ) ;
390
+ const proc = await container . spawn ( commandAndArgs [ 0 ] , commandAndArgs . slice ( 1 ) ) ;
391
+ const abortListener : ( ) => void = ( ) => proc . kill ( ) ;
392
+ abortSignal . addEventListener ( 'abort' , ( ) => {
393
+ logger . info ( 'aborting' , commandAndArgs ) ;
394
+ proc . kill ( ) ;
395
+ } ) ;
396
+ const { output, exitCode } = await streamOutput ( proc , { onOutput, debounceMs : 50 } ) ;
397
+
398
+ const cleanedOutput = cleanConvexOutput ( output ) ;
399
+ const time = performance . now ( ) - t0 ;
400
+ logger . debug ( 'finished' , errorPrefix , 'in' , Math . round ( time ) ) ;
401
+ if ( exitCode !== 0 ) {
402
+ // Kill all other commands
403
+ commandErroredController . abort ( `${ errorPrefix } ` ) ;
404
+ // This command's output will be reported exclusively
405
+ throw new Error ( `[${ errorPrefix } ] Failed with exit code ${ exitCode } : ${ cleanedOutput } ` ) ;
406
+ }
407
+ abortSignal . removeEventListener ( 'abort' , abortListener ) ;
408
+ if ( cleanedOutput . trim ( ) . length === 0 ) {
409
+ return '' ;
410
+ }
411
+ return cleanedOutput + '\n\n' ;
412
+ } ;
413
+
414
+ // START deploy tool call
415
+ // / \
416
+ // / \
417
+ // codegen \ `convex typecheck` includes typecheck of convex/ dir
418
+ // + typecheck \
419
+ // | ESLint `eslint` must not include rules that check imports
420
+ // | /
421
+ // app typecheck / `tsc --noEmit --project tsconfig.app.json
422
+ // \ /
423
+ // \ /
424
+ // deploy `deploy` can fail
425
+
426
+ // ESLint doesn't need to wait for codegen since we don't use rules like plugin-import to validate imports.
427
+ const runEslint = async ( ) => {
428
+ if ( await hasMatchingEslintConfig ( container ) ) {
429
+ // ESLint results don't stream to the terminal
430
+ return await run ( [ 'eslint' , 'convex' ] , outputLabels . convexLint ) ;
431
+ }
432
+ return '' ;
433
+ } ;
434
+
435
+ const runCodegenAndTypecheck = async ( onOutput ?: ( output : string ) => void ) => {
436
+ // Convex codegen does a convex directory typecheck, then tsc does a full-project typecheck.
437
+ let output = await run ( [ 'convex' , 'codegen' ] , outputLabels . frontendTypecheck , onOutput ) ;
438
+ output += await run (
439
+ [ 'tsc' , '--noEmit' , '-p' , 'tsconfig.app.json' ] ,
440
+ outputLabels . frontendTypecheck ,
441
+ onOutput ,
442
+ ) ;
443
+ return output ;
444
+ } ;
445
+
446
+ const t0 = performance . now ( ) ;
447
+ const [ eslintResult , codegenResult ] = await Promise . all ( [
448
+ runEslint ( ) ,
449
+ runCodegenAndTypecheck ( ( output ) => {
450
+ console . log ( 'runing terminaloutput.set() with' , output . length , 'characters' ) ;
385
451
this . terminalOutput . set ( output ) ;
386
- } ,
387
- debounceMs : 50 ,
388
- } ) ;
389
- const cleanedOutput = cleanConvexOutput ( output ) ;
390
- if ( exitCode !== 0 ) {
391
- throw new Error ( `Convex failed with exit code ${ exitCode } : ${ cleanedOutput } ` ) ;
392
- }
393
- result = cleanedOutput ;
452
+ } ) ,
453
+ ] ) ;
454
+ result += codegenResult ;
455
+ result += eslintResult ;
456
+ result += await run ( [ 'convex' , 'dev' , '--once' , '--typecheck=disable' ] , outputLabels . convexDeploy ) ;
457
+ const time = performance . now ( ) - t0 ;
458
+ logger . info ( 'deploy action finished in' , time ) ;
394
459
395
460
// Start the default preview if it’s not already running
396
461
if ( ! workbenchStore . isDefaultPreviewRunning ( ) ) {
397
462
const shell = this . #shellTerminal( ) ;
398
- await shell . startCommand ( 'npx vite --open' ) ;
463
+ await shell . startCommand ( 'vite --open' ) ;
399
464
result += '\n\nDev server started successfully!' ;
400
465
}
401
466
@@ -463,3 +528,16 @@ function cleanConvexOutput(output: string) {
463
528
}
464
529
return result ;
465
530
}
531
+
532
+ async function hasMatchingEslintConfig ( container : WebContainer ) : Promise < boolean > {
533
+ // Only run eslint if the file we expect is present and contains '@convex-dev/eslint-plugin'.
534
+ let contents = '' ;
535
+ try {
536
+ contents = await container . fs . readFile ( 'eslint.config.js' , 'utf-8' ) ;
537
+ } catch ( e : any ) {
538
+ if ( ! e . message . includes ( 'ENOENT: no such file or directory' ) ) {
539
+ throw e ;
540
+ }
541
+ }
542
+ return contents . includes ( '@convex-dev/eslint-plugin' ) ;
543
+ }
0 commit comments