22// MIT-style license that can be found in the LICENSE file or at
33// https://opensource.org/licenses/MIT.
44
5- import { spawn } from 'child_process' ;
5+ import * as child_process from 'child_process' ;
6+
67import { Observable } from 'rxjs' ;
78import { takeUntil } from 'rxjs/operators' ;
89
@@ -36,21 +37,28 @@ const initFlag = Symbol();
3637/** An asynchronous wrapper for the embedded Sass compiler */
3738export class AsyncCompiler {
3839 /** The underlying process that's being wrapped. */
39- private readonly process = spawn (
40- compilerCommand [ 0 ] ,
41- [ ...compilerCommand . slice ( 1 ) , '--embedded' ] ,
42- {
40+ private readonly process = ( ( ) => {
41+ let command = compilerCommand [ 0 ] ;
42+ let args = [ ...compilerCommand . slice ( 1 ) , '--embedded' ] ;
43+ const options : child_process . SpawnOptions = {
4344 // Use the command's cwd so the compiler survives the removal of the
4445 // current working directory.
4546 // https://github.com/sass/embedded-host-node/pull/261#discussion_r1438712923
4647 cwd : path . dirname ( compilerCommand [ 0 ] ) ,
47- // Node blocks launching .bat and .cmd without a shell due to CVE-2024-27980
48- shell : [ '.bat' , '.cmd' ] . includes (
49- path . extname ( compilerCommand [ 0 ] ) . toLowerCase ( ) ,
50- ) ,
5148 windowsHide : true ,
52- } ,
53- ) ;
49+ } ;
50+
51+ // Node forbids launching .bat and .cmd without a shell due to CVE-2024-27980,
52+ // and DEP0190 forbids passing an argument list *with* shell: true. To work
53+ // around this, we have to manually concatenate the arguments.
54+ if ( [ '.bat' , '.cmd' ] . includes ( path . extname ( command ) . toLowerCase ( ) ) ) {
55+ command = `${ command } ${ args ! . join ( ' ' ) } ` ;
56+ args = [ ] ;
57+ options . shell = true ;
58+ }
59+
60+ return child_process . spawn ( command , args , options ) ;
61+ } ) ( ) ;
5462
5563 /** The next compilation ID. */
5664 private compilationId = 1 ;
@@ -73,17 +81,17 @@ export class AsyncCompiler {
7381
7482 /** The buffers emitted by the child process's stdout. */
7583 private readonly stdout$ = new Observable < Buffer > ( observer => {
76- this . process . stdout . on ( 'data' , buffer => observer . next ( buffer ) ) ;
84+ this . process . stdout ! . on ( 'data' , buffer => observer . next ( buffer ) ) ;
7785 } ) . pipe ( takeUntil ( this . exit$ ) ) ;
7886
7987 /** The buffers emitted by the child process's stderr. */
8088 private readonly stderr$ = new Observable < Buffer > ( observer => {
81- this . process . stderr . on ( 'data' , buffer => observer . next ( buffer ) ) ;
89+ this . process . stderr ! . on ( 'data' , buffer => observer . next ( buffer ) ) ;
8290 } ) . pipe ( takeUntil ( this . exit$ ) ) ;
8391
8492 /** Writes `buffer` to the child process's stdin. */
8593 private writeStdin ( buffer : Buffer ) : void {
86- this . process . stdin . write ( buffer ) ;
94+ this . process . stdin ! . write ( buffer ) ;
8795 }
8896
8997 /** Guards against using a disposed compiler. */
@@ -190,7 +198,7 @@ export class AsyncCompiler {
190198 async dispose ( ) : Promise < void > {
191199 this . disposed = true ;
192200 await Promise . all ( this . compilations ) ;
193- this . process . stdin . end ( ) ;
201+ this . process . stdin ! . end ( ) ;
194202 await this . exit$ ;
195203 }
196204}
0 commit comments