@@ -17,6 +17,7 @@ import { z } from 'zod';
1717import { editToolParameters } from './editTool' ;
1818import { getAbsolutePath } from '~/lib/stores/files' ;
1919import { streamOutput } from '~/utils/process' ;
20+ import { outputLabels , type OutputLabels } from './deployToolOutputLabels' ;
2021
2122const logger = createScopedLogger ( 'ActionRunner' ) ;
2223
@@ -372,30 +373,94 @@ export class ActionRunner {
372373 case 'deploy' : {
373374 const container = await this . #webcontainer;
374375 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- } ) ;
382376
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' ) ;
385451 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 ) ;
394459
395460 // Start the default preview if it’s not already running
396461 if ( ! workbenchStore . isDefaultPreviewRunning ( ) ) {
397462 const shell = this . #shellTerminal( ) ;
398- await shell . startCommand ( 'npx vite --open' ) ;
463+ await shell . startCommand ( 'vite --open' ) ;
399464 result += '\n\nDev server started successfully!' ;
400465 }
401466
@@ -463,3 +528,16 @@ function cleanConvexOutput(output: string) {
463528 }
464529 return result ;
465530}
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