Skip to content

Commit 278e92e

Browse files
committed
JavaScript: Windows compatibility when spawning npm etc.
On Windows we need to spawn `npm.cmd` if we don't want to use `{ shell: true }`.
1 parent e32c921 commit 278e92e

4 files changed

Lines changed: 52 additions & 4 deletions

File tree

rewrite-javascript/rewrite/src/javascript/format/prettier-config-loader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as os from 'os';
2020
import {spawnSync} from 'child_process';
2121
import {prettierStyle, PrettierStyle} from '../style';
2222
import {randomId} from '../../uuid';
23+
import {getPlatformCommand} from '../../shell-utils';
2324

2425
/**
2526
* Cache of loaded Prettier modules by version.
@@ -97,7 +98,7 @@ async function installPrettierToCache(version: string): Promise<void> {
9798
);
9899

99100
// Run npm install
100-
const result = spawnSync('npm', ['install', '--silent'], {
101+
const result = spawnSync(getPlatformCommand('npm'), ['install', '--silent'], {
101102
cwd: cacheDir,
102103
encoding: 'utf-8',
103104
stdio: ['pipe', 'pipe', 'pipe'],

rewrite-javascript/rewrite/src/javascript/package-manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {PlainTextParser} from "../text";
2828
import {SourceFile} from "../tree";
2929
import {TreeVisitor} from "../visitor";
3030
import {ExecutionContext} from "../execution";
31+
import {getPlatformCommand} from "../shell-utils";
3132
import * as fs from "fs";
3233
import * as fsp from "fs/promises";
3334
import * as path from "path";
@@ -231,7 +232,7 @@ function runInstall(pm: PackageManager, options: InstallOptions): PackageManager
231232
const [cmd, ...args] = command;
232233

233234
try {
234-
const result = spawnSync(cmd, args, {
235+
const result = spawnSync(getPlatformCommand(cmd), args, {
235236
cwd: options.cwd,
236237
encoding: 'utf-8',
237238
stdio: ['pipe', 'pipe', 'pipe'],
@@ -285,7 +286,7 @@ export function runList(pm: PackageManager, cwd: string, timeout: number = 30000
285286

286287
const [cmd, ...args] = config.listCommand;
287288

288-
const result = spawnSync(cmd, args, {
289+
const result = spawnSync(getPlatformCommand(cmd), args, {
289290
cwd,
290291
encoding: 'utf-8',
291292
stdio: ['pipe', 'pipe', 'pipe'],

rewrite-javascript/rewrite/src/rpc/request/install-recipes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as fs from "fs";
1919
import {spawn} from "child_process";
2020
import {withMetrics} from "./metrics";
2121
import {RecipeMarketplace} from "../../marketplace";
22+
import {getPlatformCommand} from "../../shell-utils";
2223

2324
export interface InstallRecipesResponse {
2425
recipesInstalled: number
@@ -35,7 +36,7 @@ async function spawnNpmCommand(
3536
logger?: rpc.Logger,
3637
logPrefix?: string
3738
): Promise<void> {
38-
const child = spawn(command, args, {cwd});
39+
const child = spawn(getPlatformCommand(command), args, {cwd});
3940

4041
if (logger) {
4142
const prefix = logPrefix ? `${logPrefix}: ` : '';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import * as os from 'os';
17+
18+
/**
19+
* Utility functions for cross-platform shell command execution.
20+
*/
21+
22+
/**
23+
* Commands that are implemented as .cmd scripts on Windows.
24+
* These need a .cmd suffix when spawned without `shell: true`.
25+
*/
26+
const CMD_SCRIPT_COMMANDS = new Set(['npm', 'npx', 'yarn', 'pnpm', 'pnpx']);
27+
28+
/**
29+
* Returns the correct command name for the current platform.
30+
* On Windows, Node.js package manager commands (npm, npx, yarn, pnpm) are
31+
* implemented as .cmd scripts and require the .cmd extension when spawned
32+
* without `shell: true`.
33+
*
34+
* Note: `bun` uses a native executable (bun.exe) on Windows, not a .cmd script,
35+
* so it doesn't need special handling.
36+
*
37+
* @param command The base command name (e.g., 'npm', 'yarn', 'pnpm')
38+
* @returns The platform-appropriate command name
39+
*/
40+
export function getPlatformCommand(command: string): string {
41+
if (os.platform() === 'win32' && CMD_SCRIPT_COMMANDS.has(command)) {
42+
return `${command}.cmd`;
43+
}
44+
return command;
45+
}

0 commit comments

Comments
 (0)