Skip to content

Commit 3afea5c

Browse files
authored
Merge pull request #395 from sass/child-process-deprecation
Fix Node.js deprecation warnings about passing args to child_process
2 parents b513e88 + 6cb356a commit 3afea5c

File tree

6 files changed

+75
-44
lines changed

6 files changed

+75
-44
lines changed

bin/sass.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,31 @@
22

33
import * as child_process from 'child_process';
44
import * as path from 'path';
5+
56
import {compilerCommand} from '../lib/src/compiler-path';
67

78
// TODO npm/cmd-shim#152 and yarnpkg/berry#6422 - If and when the package
89
// managers support it, we should make this a proper shell script rather than a
910
// JS wrapper.
1011

1112
try {
12-
child_process.execFileSync(
13-
compilerCommand[0],
14-
[...compilerCommand.slice(1), ...process.argv.slice(2)],
15-
{
16-
// Node blocks launching .bat and .cmd without a shell due to CVE-2024-27980
17-
shell: ['.bat', '.cmd'].includes(
18-
path.extname(compilerCommand[0]).toLowerCase(),
19-
),
20-
stdio: 'inherit',
21-
windowsHide: true,
22-
},
23-
);
13+
let command = compilerCommand[0];
14+
let args = [...compilerCommand.slice(1), ...process.argv.slice(2)];
15+
const options: child_process.ExecFileSyncOptions = {
16+
stdio: 'inherit',
17+
windowsHide: true,
18+
};
19+
20+
// Node forbids launching .bat and .cmd without a shell due to CVE-2024-27980,
21+
// and DEP0190 forbids passing an argument list *with* shell: true. To work
22+
// around this, we have to manually concatenate the arguments.
23+
if (['.bat', '.cmd'].includes(path.extname(command).toLowerCase())) {
24+
command = `${command} ${args.join(' ')}`;
25+
args = [];
26+
options.shell = true;
27+
}
28+
29+
child_process.execFileSync(command, args, options);
2430
} catch (error) {
2531
if (error.code) {
2632
throw error;

lib/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function defaultExportDeprecation() {
4747
printedDefaultExportDeprecation = true;
4848
console.error(
4949
"`import sass from 'sass'` is deprecated.\n" +
50-
"Please use `import * as sass from 'sass'` instead."
50+
"Please use `import * as sass from 'sass'` instead.",
5151
);
5252
}
5353

lib/src/compiler/async.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
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+
67
import {Observable} from 'rxjs';
78
import {takeUntil} from 'rxjs/operators';
89

@@ -36,21 +37,28 @@ const initFlag = Symbol();
3637
/** An asynchronous wrapper for the embedded Sass compiler */
3738
export 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
}

lib/src/compiler/sync.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import * as path from 'path';
6+
57
import {Subject} from 'rxjs';
6-
import {SyncChildProcess} from 'sync-child-process';
8+
import * as sync_child_process from 'sync-child-process';
79

8-
import * as path from 'path';
910
import {
1011
OptionsWithLegacy,
1112
createDispatcher,
@@ -36,21 +37,28 @@ const initFlag = Symbol();
3637
/** A synchronous wrapper for the embedded Sass compiler */
3738
export class Compiler {
3839
/** The underlying process that's being wrapped. */
39-
private readonly process = new SyncChildProcess(
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: sync_child_process.Options = {
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 new sync_child_process.SyncChildProcess(command, args, options);
61+
})();
5462

5563
/** The next compilation ID. */
5664
private compilationId = 1;

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sass-embedded",
3-
"version": "1.93.2",
3+
"version": "1.93.3-dev",
44
"protocol-version": "3.2.0",
55
"compiler-version": "1.93.2",
66
"description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol",
@@ -28,12 +28,15 @@
2828
},
2929
"scripts": {
3030
"init": "ts-node ./tool/init.ts",
31-
"check": "npm-run-all check:gts check:tsc",
31+
"check": "npm-run-all check:gts check:tsc check:mjs",
3232
"check:gts": "gts check",
3333
"check:tsc": "tsc --noEmit",
34+
"check:mjs": "prettier -0check **/*.mjs",
3435
"clean": "gts clean",
3536
"compile": "tsc -p tsconfig.build.json && cp lib/index.mjs dist/lib/index.mjs && cp -r lib/src/vendor/sass/ dist/lib/src/vendor/sass && cp dist/lib/src/vendor/sass/index.d.ts dist/lib/src/vendor/sass/index.m.d.ts",
36-
"fix": "gts fix",
37+
"fix": "npm-run-all fix:ts fix:mjs",
38+
"fix:ts": "gts fix",
39+
"fix:mjs": "prettier --write **/*.mjs",
3740
"prepublishOnly": "npm run clean && ts-node ./tool/prepare-release.ts",
3841
"test": "jest"
3942
},

test/after-compile-test.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ const cjs = await import('../dist/lib/index.js');
1818
const esm = await import('../dist/lib/index.mjs');
1919

2020
for (const [name, value] of Object.entries(cjs)) {
21-
if (name === '__esModule' || name === 'default') continue;
21+
if (
22+
name === '__esModule' ||
23+
name === 'default' ||
24+
name === 'module.exports'
25+
) {
26+
continue;
27+
}
2228
if (!esm[name]) {
2329
throw new Error(`ESM module is missing export ${name}.`);
2430
} else if (esm[name] !== value) {

0 commit comments

Comments
 (0)