From 60cea0473f7626dcf6439e770cc9b895fcfd48c2 Mon Sep 17 00:00:00 2001 From: Josh Faigan Date: Thu, 5 Feb 2026 14:16:01 -0500 Subject: [PATCH] wip --- .../cli-kit/src/public/node/system.test.ts | 44 ++++++++++++++++++ packages/cli-kit/src/public/node/system.ts | 18 ++++++++ .../theme/src/cli/services/profile.test.ts | 46 ++++++++++++++++++- packages/theme/src/cli/services/profile.ts | 25 ++++++++-- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/packages/cli-kit/src/public/node/system.test.ts b/packages/cli-kit/src/public/node/system.test.ts index 6019d294ae9..c0e5eeac30c 100644 --- a/packages/cli-kit/src/public/node/system.test.ts +++ b/packages/cli-kit/src/public/node/system.test.ts @@ -113,3 +113,47 @@ describe('readStdinString', () => { expect(got).toBe('hello world') }) }) + +describe('convertWslPath', () => { + test('converts Linux path to Windows path successfully', async () => { + // Given + const linuxPath = '/tmp/speedscope-123.html' + const windowsPath = 'C:\\Users\\developer\\AppData\\Local\\Temp\\speedscope-123.html' + vi.mocked(which.sync).mockReturnValueOnce('/usr/bin/wslpath') + vi.mocked(execa).mockResolvedValueOnce({stdout: windowsPath + '\n'} as any) + + // When + const result = await system.convertWslPath(linuxPath) + + // Then + expect(result).toBe(windowsPath) + expect(execa).toHaveBeenCalledWith('wslpath', ['-w', linuxPath], expect.any(Object)) + }) + + test('returns original path if wslpath command fails', async () => { + // Given + const linuxPath = '/tmp/speedscope-123.html' + vi.mocked(which.sync).mockReturnValueOnce('/usr/bin/wslpath') + vi.mocked(execa).mockRejectedValueOnce(new Error('wslpath not found')) + + // When + const result = await system.convertWslPath(linuxPath) + + // Then + expect(result).toBe(linuxPath) + }) + + test('trims whitespace from wslpath output', async () => { + // Given + const linuxPath = '/tmp/file.html' + const windowsPath = 'C:\\Temp\\file.html' + vi.mocked(which.sync).mockReturnValueOnce('/usr/bin/wslpath') + vi.mocked(execa).mockResolvedValueOnce({stdout: ` ${windowsPath} \n`} as any) + + // When + const result = await system.convertWslPath(linuxPath) + + // Then + expect(result).toBe(windowsPath) + }) +}) diff --git a/packages/cli-kit/src/public/node/system.ts b/packages/cli-kit/src/public/node/system.ts index b9064174315..5ededef8743 100644 --- a/packages/cli-kit/src/public/node/system.ts +++ b/packages/cli-kit/src/public/node/system.ts @@ -202,6 +202,24 @@ export async function isWsl(): Promise { return wsl.default } +/** + * Convert a WSL Linux path to a Windows path using wslpath. + * This is useful when opening files in Windows browsers from WSL. + * + * @param linuxPath - The Linux path to convert (e.g., /tmp/file.html). + * @returns A promise that resolves with the Windows path. + */ +export async function convertWslPath(linuxPath: string): Promise { + try { + const windowsPath = await captureOutput('wslpath', ['-w', linuxPath]) + return windowsPath.trim() + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (error) { + outputDebug(`Failed to convert WSL path using wslpath: ${error}. Falling back to original Linux path: ${linuxPath}`) + return linuxPath + } +} + /** * Check if stdin has piped data available. * This distinguishes between actual piped input (e.g., `echo "query" | cmd`) diff --git a/packages/theme/src/cli/services/profile.test.ts b/packages/theme/src/cli/services/profile.test.ts index fdf86ce1af1..0b5cff08048 100644 --- a/packages/theme/src/cli/services/profile.test.ts +++ b/packages/theme/src/cli/services/profile.test.ts @@ -8,7 +8,15 @@ import {AbortError} from '@shopify/cli-kit/node/error' import {readFile} from 'fs/promises' vi.mock('@shopify/cli-kit/node/session') -vi.mock('@shopify/cli-kit/node/system') +vi.mock('@shopify/cli-kit/node/system', async () => { + const actual = await vi.importActual('@shopify/cli-kit/node/system') + return { + ...actual, + openURL: vi.fn().mockResolvedValue(true), + isWsl: vi.fn().mockResolvedValue(false), + convertWslPath: vi.fn((path: string) => Promise.resolve(path)), + } +}) vi.mock('@shopify/cli-kit/node/output') vi.mock('../utilities/theme-environment/storefront-password-prompt.js') vi.mock('../utilities/theme-environment/storefront-session.js') @@ -82,6 +90,42 @@ describe('profile', () => { expect(htmlContent).toContain('speedscope') }) + test('converts all WSL paths before opening browser', async () => { + // Given + const {isWsl, convertWslPath} = await import('@shopify/cli-kit/node/system') + vi.mocked(isWsl).mockResolvedValue(true) + vi.mocked(convertWslPath).mockImplementation((linuxPath: string) => { + if (linuxPath.endsWith('.html')) { + return Promise.resolve('C:\\Users\\dev\\AppData\\Local\\Temp\\speedscope-12345-999.html') + } + if (linuxPath.endsWith('.js')) { + return Promise.resolve('C:\\Users\\dev\\AppData\\Local\\Temp\\speedscope-12345-999.js') + } + // speedscope index.html asset path + return Promise.resolve('C:\\Users\\dev\\node_modules\\speedscope\\dist\\release\\index.html') + }) + + // When + await profile(mockAdminSession, themeId, urlPath, false, undefined, undefined) + + // Then + expect(isWsl).toHaveBeenCalled() + // All 3 paths should be converted + expect(convertWslPath).toHaveBeenCalledTimes(3) + expect(convertWslPath).toHaveBeenCalledWith(expect.stringContaining('index.html')) + expect(convertWslPath).toHaveBeenCalledWith(expect.stringMatching(/\.js$/)) + expect(convertWslPath).toHaveBeenCalledWith(expect.stringMatching(/\.html$/)) + expect(openURL).toHaveBeenCalledWith('file://C:\\Users\\dev\\AppData\\Local\\Temp\\speedscope-12345-999.html') + + // Verify the redirect HTML contains Windows paths (not Linux paths) + const openUrlCalls = vi.mocked(openURL).mock.calls + const firstCall = openUrlCalls[0] + if (!firstCall) throw new Error('Expected at least one openURL call') + const convertWslPathCalls = vi.mocked(convertWslPath).mock.calls + expect(convertWslPathCalls.some((call) => String(call[0]).endsWith('.js'))).toBe(true) + expect(convertWslPathCalls.some((call) => String(call[0]).includes('index.html'))).toBe(true) + }) + test('throws error when fetch fails', async () => { // Given vi.mocked(render).mockRejectedValue(new Error('Network error')) diff --git a/packages/theme/src/cli/services/profile.ts b/packages/theme/src/cli/services/profile.ts index 5aa13179c8a..c904b863857 100644 --- a/packages/theme/src/cli/services/profile.ts +++ b/packages/theme/src/cli/services/profile.ts @@ -3,7 +3,7 @@ import {ensureValidPassword} from '../utilities/theme-environment/storefront-pas import {fetchDevServerSession} from '../utilities/theme-environment/dev-server-session.js' import {render} from '../utilities/theme-environment/storefront-renderer.js' import {resolveAssetPath} from '../utilities/asset-path.js' -import {openURL} from '@shopify/cli-kit/node/system' +import {openURL, isWsl, convertWslPath} from '@shopify/cli-kit/node/system' import {joinPath} from '@shopify/cli-kit/node/path' import {AdminSession} from '@shopify/cli-kit/node/session' import {writeFile, tempDirectory} from '@shopify/cli-kit/node/fs' @@ -69,7 +69,20 @@ async function openProfile(profileJson: string) { outputDebug(`[Theme Profile] writing JS file to: ${jsPath}`) await writeFile(jsPath, jsSource) outputDebug(`[Theme Profile] JS file created successfully: ${jsPath}`) - urlToOpen += `#localProfilePath=${jsPath}` + + // In WSL, we need to convert Linux paths to Windows paths for the browser to access them. + const wsl = await isWsl() + if (wsl) { + const windowsSpeedscopePath = await convertWslPath(urlToOpen) + outputDebug(`[Theme Profile] Converted WSL speedscope path: ${urlToOpen} -> ${windowsSpeedscopePath}`) + urlToOpen = windowsSpeedscopePath + + const windowsJsPath = await convertWslPath(jsPath) + outputDebug(`[Theme Profile] Converted WSL JS path: ${jsPath} -> ${windowsJsPath}`) + urlToOpen += `#localProfilePath=${windowsJsPath}` + } else { + urlToOpen += `#localProfilePath=${jsPath}` + } // For some silly reason, the OS X open command ignores any query parameters or hash parameters // passed as part of the URL. To get around this weird issue, we'll create a local HTML file @@ -79,7 +92,13 @@ async function openProfile(profileJson: string) { await writeFile(htmlPath, ``) outputDebug(`[Theme Profile] HTML file created successfully: ${htmlPath}`) - urlToOpen = `file://${htmlPath}` + let pathToOpen = htmlPath + if (wsl) { + pathToOpen = await convertWslPath(htmlPath) + outputDebug(`[Theme Profile] Converted WSL HTML path: ${htmlPath} -> ${pathToOpen}`) + } + + urlToOpen = `file://${pathToOpen}` outputDebug(`[Theme Profile] Opening URL: ${urlToOpen}`) const opened = await openURL(urlToOpen) outputDebug(`[Theme Profile] URL opened successfully: ${opened}`)