1- import type { DevToolsChildProcessTerminalOptions , DevToolsChildProcessTerminalSession , DevToolsNodeContext , DevToolsTerminalHost as DevToolsTerminalHostType , DevToolsTerminalSession , DevToolsTerminalSessionSerializable } from '@vitejs/devtools-kit'
1+ import type { DevToolsChildProcessExecuteOptions , DevToolsChildProcessTerminalSession , DevToolsNodeContext , DevToolsTerminalHost as DevToolsTerminalHostType , DevToolsTerminalSession , DevToolsTerminalSessionBase , PartialWithoutId } from '@vitejs/devtools-kit'
22import type { Result as TinyExecResult } from 'tinyexec'
33import process from 'node:process'
4+ import { createEventEmitter } from '@vitejs/devtools-kit/utils/events'
45
56export class DevToolsTerminalHost implements DevToolsTerminalHostType {
7+ public readonly sessions : DevToolsTerminalHostType [ 'sessions' ] = new Map ( )
8+ public readonly events : DevToolsTerminalHostType [ 'events' ] = createEventEmitter ( )
9+
10+ private _boundStreams = new Map < string , {
11+ dispose : ( ) => void
12+ stream : ReadableStream
13+ } > ( )
14+
615 constructor (
716 public readonly context : DevToolsNodeContext ,
817 ) {
918 }
1019
11- readonly sessions : Map < string , DevToolsTerminalSession > = new Map ( )
12-
13- serialize ( session : DevToolsTerminalSession ) : DevToolsTerminalSessionSerializable {
14- return {
15- id : session . id ,
16- title : session . title ,
17- description : session . description ,
18- status : session . status ,
19- buffer : session . buffer ?? [ ] ,
20- }
21- }
22-
2320 register ( session : DevToolsTerminalSession ) : DevToolsTerminalSession {
2421 if ( this . sessions . has ( session . id ) ) {
2522 throw new Error ( `Terminal session with id "${ session . id } " already registered` )
2623 }
2724 this . sessions . set ( session . id , session )
25+ this . bindStream ( session )
26+ this . events . emit ( 'terminal:session:updated' , session )
2827 return session
2928 }
3029
31- update ( session : DevToolsTerminalSession ) : void {
32- if ( ! this . sessions . has ( session . id ) ) {
33- throw new Error ( `Terminal session with id "${ session . id } " not registered` )
30+ update ( patch : PartialWithoutId < DevToolsTerminalSession > ) : void {
31+ if ( ! this . sessions . has ( patch . id ) ) {
32+ throw new Error ( `Terminal session with id "${ patch . id } " not registered` )
3433 }
35- this . sessions . set ( session . id , session )
34+ const session = this . sessions . get ( patch . id ) !
35+ Object . assign ( session , patch )
36+ this . sessions . set ( patch . id , session )
37+ this . bindStream ( session )
38+ this . events . emit ( 'terminal:session:updated' , session )
39+ }
40+
41+ remove ( session : DevToolsTerminalSession ) : void {
42+ this . sessions . delete ( session . id )
43+ this . events . emit ( 'terminal:session:updated' , session )
44+ this . _boundStreams . delete ( session . id )
45+ }
46+
47+ private bindStream ( session : DevToolsTerminalSession ) {
48+ // Skip when the same stream is already bound
49+ if ( this . _boundStreams . has ( session . id ) && this . _boundStreams . get ( session . id ) ?. stream === session . stream )
50+ return
51+
52+ // Dispose the previous stream
53+ this . _boundStreams . get ( session . id ) ?. dispose ( )
54+ this . _boundStreams . delete ( session . id )
55+
56+ // If new stream is not available, skip
57+ if ( ! session . stream )
58+ return
59+
60+ session . buffer ||= [ ]
61+ const events = this . events
62+ const writer = new WritableStream < string > ( {
63+ write ( chunk ) {
64+ session . buffer ! . push ( chunk )
65+ events . emit ( 'terminal:session:stream-chunk' , {
66+ id : session . id ,
67+ chunks : [ chunk ] ,
68+ ts : Date . now ( ) ,
69+ } )
70+ } ,
71+ } )
72+ session . stream . pipeTo ( writer )
73+ this . _boundStreams . set ( session . id , {
74+ dispose : ( ) => {
75+ writer . close ( )
76+ } ,
77+ stream : session . stream ,
78+ } )
3679 }
3780
38- async startChildProcess ( options : DevToolsChildProcessTerminalOptions ) : Promise < DevToolsChildProcessTerminalSession > {
39- if ( this . sessions . has ( options . id ) ) {
40- throw new Error ( `Terminal session with id "${ options . id } " already registered` )
81+ async startChildProcess (
82+ executeOptions : DevToolsChildProcessExecuteOptions ,
83+ terminal : DevToolsTerminalSessionBase ,
84+ ) : Promise < DevToolsChildProcessTerminalSession > {
85+ if ( this . sessions . has ( terminal . id ) ) {
86+ throw new Error ( `Terminal session with id "${ terminal . id } " already registered` )
4187 }
4288 const { exec } = await import ( 'tinyexec' )
4389
@@ -55,16 +101,16 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType {
55101
56102 function createChildProcess ( ) {
57103 const cp = exec (
58- options . command ,
59- options . args || [ ] ,
104+ executeOptions . command ,
105+ executeOptions . args || [ ] ,
60106 {
61107 nodeOptions : {
62108 env : {
63109 COLORS : 'true' ,
64110 FORCE_COLOR : 'true' ,
65- ...( options . env || { } ) ,
111+ ...( executeOptions . env || { } ) ,
66112 } ,
67- cwd : options . cwd ?? process . cwd ( ) ,
113+ cwd : executeOptions . cwd ?? process . cwd ( ) ,
68114 stdio : 'pipe' ,
69115 } ,
70116 } ,
@@ -93,9 +139,11 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType {
93139 }
94140
95141 const session : DevToolsChildProcessTerminalSession = {
96- ...options ,
142+ ...terminal ,
97143 status : 'running' ,
98144 stream : buffer ,
145+ type : 'child-process' ,
146+ executeOptions,
99147 getChildProcess : ( ) => cp ?. process ,
100148 terminate,
101149 restart,
0 commit comments