Skip to content
Draft

wip #6830

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/cli-kit/src/public/node/system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
18 changes: 18 additions & 0 deletions packages/cli-kit/src/public/node/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,24 @@ export async function isWsl(): Promise<boolean> {
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<string> {
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`)
Expand Down
46 changes: 45 additions & 1 deletion packages/theme/src/cli/services/profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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'))
Expand Down
25 changes: 22 additions & 3 deletions packages/theme/src/cli/services/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -79,7 +92,13 @@ async function openProfile(profileJson: string) {
await writeFile(htmlPath, `<script>window.location=${JSON.stringify(urlToOpen)}</script>`)
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}`)
Expand Down
Loading