1
- import { DestroyRef , inject , Injectable } from '@angular/core' ;
1
+ import { DestroyRef , inject , Injectable , signal } from '@angular/core' ;
2
2
import { takeUntilDestroyed } from '@angular/core/rxjs-interop' ;
3
3
import {
4
4
type FileSystemTree ,
@@ -9,13 +9,19 @@ import {
9
9
import { Terminal } from '@xterm/xterm' ;
10
10
import {
11
11
BehaviorSubject ,
12
+ combineLatest ,
12
13
combineLatestWith ,
13
14
distinctUntilChanged ,
14
15
filter ,
15
16
from ,
17
+ fromEvent ,
16
18
map ,
17
19
Observable ,
18
- switchMap
20
+ of ,
21
+ skip ,
22
+ switchMap ,
23
+ take ,
24
+ timeout
19
25
} from 'rxjs' ;
20
26
import { withLatestFrom } from 'rxjs/operators' ;
21
27
import { createTerminalStream , killTerminal , makeProcessWritable } from './webcontainer.helpers' ;
@@ -48,9 +54,17 @@ export class WebContainerRunner {
48
54
} ;
49
55
private watcher : IFSWatcher | null = null ;
50
56
57
+ private readonly progressWritable = signal ( { currentStep : 0 , totalSteps : 3 , label : 'Initializing web-container' } ) ;
58
+
59
+ /**
60
+ * Progress indicator of the loading of a project.
61
+ */
62
+ public readonly progress = this . progressWritable . asReadonly ( ) ;
63
+
51
64
constructor ( ) {
52
65
const destroyRef = inject ( DestroyRef ) ;
53
66
this . instancePromise = WebContainer . boot ( ) . then ( ( instance ) => {
67
+ this . progressWritable . update ( ( { totalSteps} ) => ( { currentStep : 1 , totalSteps, label : 'Loading project' } ) ) ;
54
68
// eslint-disable-next-line no-console
55
69
const unsubscribe = instance . on ( 'error' , console . error ) ;
56
70
destroyRef . onDestroy ( ( ) => unsubscribe ( ) ) ;
@@ -65,20 +79,33 @@ export class WebContainerRunner {
65
79
void this . runCommand ( commandElements [ 0 ] , commandElements . slice ( 1 ) , cwd ) ;
66
80
} ) ;
67
81
68
- this . iframe . pipe (
69
- filter ( ( iframe ) : iframe is HTMLIFrameElement => ! ! iframe ) ,
70
- distinctUntilChanged ( ) ,
71
- withLatestFrom ( this . instancePromise ) ,
72
- switchMap ( ( [ iframe , instance ] ) => new Observable ( ( subscriber ) => {
82
+ combineLatest ( [
83
+ this . iframe . pipe (
84
+ filter ( ( iframe ) : iframe is HTMLIFrameElement => ! ! iframe ) ,
85
+ distinctUntilChanged ( )
86
+ ) ,
87
+ this . instancePromise
88
+ ] ) . pipe (
89
+ switchMap ( ( [ iframe , instance ] ) => new Observable < [ typeof iframe , typeof instance , boolean ] > ( ( subscriber ) => {
90
+ let shouldSkipFirstLoadEvent = true ;
73
91
const serverReadyUnsubscribe = instance . on ( 'server-ready' , ( _port : number , url : string ) => {
92
+ this . progressWritable . update ( ( { totalSteps} ) => ( { currentStep : totalSteps - 1 , totalSteps, label : 'Bootstrapping application...' } ) ) ;
74
93
iframe . removeAttribute ( 'srcdoc' ) ;
75
94
iframe . src = url ;
76
- subscriber . next ( url ) ;
95
+ subscriber . next ( [ iframe , instance , shouldSkipFirstLoadEvent ] ) ;
96
+ shouldSkipFirstLoadEvent = false ;
77
97
} ) ;
78
98
return ( ) => serverReadyUnsubscribe ( ) ;
79
99
} ) ) ,
100
+ switchMap ( ( [ iframe , _instance , shouldSkipFirstLoadEvent ] ) => fromEvent ( iframe , 'load' ) . pipe (
101
+ skip ( shouldSkipFirstLoadEvent ? 1 : 0 ) ,
102
+ timeout ( { each : 20000 , with : ( ) => of ( [ ] ) } ) ,
103
+ take ( 1 )
104
+ ) ) ,
80
105
takeUntilDestroyed ( )
81
- ) . subscribe ( ) ;
106
+ ) . subscribe ( ( ) => {
107
+ this . progressWritable . update ( ( { totalSteps} ) => ( { currentStep : totalSteps , totalSteps, label : 'Ready!' } ) ) ;
108
+ } ) ;
82
109
83
110
this . commandOutput . process . pipe (
84
111
filter ( ( process ) : process is WebContainerProcess => ! ! process && ! process . output . locked ) ,
@@ -152,10 +179,35 @@ export class WebContainerRunner {
152
179
const process = await instance . spawn ( command , args , { cwd : cwd } ) ;
153
180
this . commandOutput . process . next ( process ) ;
154
181
const exitCode = await process . exit ;
155
- if ( exitCode !== 0 ) {
182
+ if ( exitCode === 0 ) {
183
+ // The process has ended successfully
184
+ this . progressWritable . update ( ( { currentStep, totalSteps} ) => ( {
185
+ currentStep : currentStep + 1 ,
186
+ totalSteps,
187
+ label : this . getCommandLabel ( this . commands . value . queue [ 1 ] )
188
+ } ) ) ;
189
+ this . commands . next ( { queue : this . commands . value . queue . slice ( 1 ) , cwd} ) ;
190
+ } else if ( exitCode === 143 ) {
191
+ // The process was killed by switching to a new project
192
+ return ;
193
+ } else {
194
+ // The process has ended with an error
156
195
throw new Error ( `Command ${ [ command , ...args ] . join ( ' ' ) } failed with ${ exitCode } !` ) ;
157
196
}
158
- this . commands . next ( { queue : this . commands . value . queue . slice ( 1 ) , cwd} ) ;
197
+ }
198
+
199
+ private getCommandLabel ( command : string ) {
200
+ if ( ! command ) {
201
+ return 'Waiting for server to start...' ;
202
+ } else {
203
+ if ( / ( n p m | y a r n ) i n s t a l l / . test ( command ) ) {
204
+ return 'Installing dependencies...' ;
205
+ } else if ( / ^ ( n p m | y a r n ) ( r u n .* : ( b u i l d | s e r v e ) | ( r u n ) ? b u i l d ) / . test ( command ) ) {
206
+ return 'Building application...' ;
207
+ } else {
208
+ return `Executing \`${ command } \`` ;
209
+ }
210
+ }
159
211
}
160
212
161
213
/**
@@ -182,6 +234,7 @@ export class WebContainerRunner {
182
234
await instance . mount ( { [ projectFolder ] : { directory : files } } ) ;
183
235
}
184
236
this . treeUpdateCallback ( ) ;
237
+ this . progressWritable . set ( { currentStep : 2 , totalSteps : 3 + commands . length , label : this . getCommandLabel ( commands [ 0 ] ) } ) ;
185
238
this . commands . next ( { queue : commands , cwd : projectFolder } ) ;
186
239
this . watcher = instance . fs . watch ( `/${ projectFolder } ` , { encoding : 'utf8' } , this . treeUpdateCallback ) ;
187
240
}
0 commit comments