fix(desktop): preserve Windows backslash paths in custom extension command (#9741)

Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-06-17 18:47:44 +10:00
committed by GitHub
parent 55dd84775b
commit f7b32dab32
2 changed files with 42 additions and 2 deletions
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
nameToKey,
getDefaultFormData,
@@ -346,6 +346,36 @@ describe('Extension Utils', () => {
])('splits %j correctly', (input, expected) => {
expect(splitCmdAndArgs(input)).toEqual(expected);
});
describe('on Windows', () => {
beforeEach(() => {
vi.stubGlobal('window', { electron: { platform: 'win32' } });
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('preserves backslashes in a Windows path', () => {
expect(splitCmdAndArgs('C:\\Users\\name\\path\\to\\extension.js')).toEqual({
cmd: 'C:\\Users\\name\\path\\to\\extension.js',
args: [],
});
});
it('preserves backslashes in cmd and args', () => {
expect(splitCmdAndArgs('node C:\\Users\\name\\ext.js')).toEqual({
cmd: 'node',
args: ['C:\\Users\\name\\ext.js'],
});
});
it('handles a quoted Windows path containing spaces', () => {
expect(splitCmdAndArgs('"C:\\Program Files\\app\\ext.js"')).toEqual({
cmd: 'C:\\Program Files\\app\\ext.js',
args: [],
});
});
});
});
describe('extractCommand', () => {
@@ -168,13 +168,23 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
}
}
function isWindowsPlatform(): boolean {
return typeof window !== 'undefined' && window.electron?.platform === 'win32';
}
export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } {
const trimmed = str.trim();
if (!trimmed) {
return { cmd: '', args: [] };
}
const parsed = parseShellQuote(trimmed);
// shell-quote treats `\` as a POSIX escape character, so a Windows path like
// `C:\Users\name\ext.js` would lose its backslashes and become `C:Usersnameext.js`.
// Doubling backslashes on Windows lets them survive parsing (shell-quote unescapes
// `\\` back to `\`), while still honoring quotes for paths containing spaces.
const toParse = isWindowsPlatform() ? trimmed.replace(/\\/g, '\\\\') : trimmed;
const parsed = parseShellQuote(toParse);
const words = parsed.filter((item): item is string => typeof item === 'string').map(String);
const cmd = words[0] || '';