Repository: marp-team/marp-vscode
Pull request #359 (2022-06-03 16:36:16)

Improve resolution of base path in workspace proxy server
I've noticed the workspace proxy server is not handling the base path of workspace URI correctly. 

In the multiple workspace, a URI for each workspace root may become `virtual-workspace://multiple-workspaces/root`. If resolved path such as `/abc.jpg`, we have to resolve the image from `virtual-workspace://multiple-workspaces/root/abc.jpg`. However, currently we wrongly resolve `virtual-workspace://multiple-workspaces/abc.jpg` by following the rule of absolute path.

This change may resolve the file resolution issue in some virtual workspaces, ~~such as #238.~~

In addition, this fix is opening the door for working on `file` scheme URI, and it brings more compatible file path resoluition with VS Code's Markdown preview to the export command. (Specifically, `/abc.jpg` can point at the workspace root, not filesystem root. It may become a breaking change to exist projects so the workspace-based resolution in `file` scheme should be an optional) 


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5ba1cf8..67d1e05 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -85,13 +85,6 @@ jobs:
     steps:
       - audit
 
-  unit-electron12:
-    executor:
-      name: node
-      version: '14.16.0'
-    steps:
-      - test
-
   unit-electron13:
     executor:
       name: node
@@ -110,9 +103,6 @@ workflows:
   test:
     jobs:
       - audit
-      - unit-electron12:
-          requires:
-            - audit
       - unit-electron13:
           requires:
             - audit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa3be53..c786e99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 
 ### Fixed
 
+- Improved base path resolving in a proxy server for virtual workspace ([#359](https://github.com/marp-team/marp-vscode/issues/359))
 - Auto-scaling is not working in the preview ([#360](https://github.com/marp-team/marp-vscode/issues/360))
 
 ## v2.0.0 - 2022-06-03
diff --git a/src/commands/export.ts b/src/commands/export.ts
index 29e30f4..69a578c 100644
--- a/src/commands/export.ts
+++ b/src/commands/export.ts
@@ -82,10 +82,19 @@ export const doExport = async (uri: Uri, document: TextDocument) => {
 
     if (workspaceFolder) {
       proxyServer = await createWorkspaceProxyServer(workspaceFolder)
-      baseUrl = `http://127.0.0.1:${proxyServer.port}${document.uri.path}`
+
+      let baseUrlPath = document.uri.path
+      if (baseUrlPath.startsWith(workspaceFolder.uri.path)) {
+        baseUrlPath = baseUrlPath.slice(workspaceFolder.uri.path.length)
+      }
+      if (!baseUrlPath.startsWith('/')) {
+        baseUrlPath = `/${baseUrlPath}`
+      }
+
+      baseUrl = `http://127.0.0.1:${proxyServer.port}${baseUrlPath}`
 
       console.debug(
-        `Proxy server for the workspace ${workspaceFolder.name} has created (port: ${proxyServer.port})`
+        `Proxy server for the workspace ${workspaceFolder.name} has created (port: ${proxyServer.port} / baseUrl: ${baseUrl})`
       )
     }
   }
diff --git a/src/workspace-proxy-server.ts b/src/workspace-proxy-server.ts
index 9a87485..0fa3664 100644
--- a/src/workspace-proxy-server.ts
+++ b/src/workspace-proxy-server.ts
@@ -2,7 +2,7 @@ import type { Server } from 'http'
 import path from 'path'
 import express from 'express'
 import { getPortPromise } from 'portfinder'
-import { FileType, workspace, WorkspaceFolder } from 'vscode'
+import { FileType, Uri, workspace, WorkspaceFolder } from 'vscode'
 
 export interface WorkspaceProxyServer {
   dispose: () => void
@@ -20,7 +20,7 @@ export const createWorkspaceProxyServer = async (
     const url = new URL(req.url, `http://${req.headers.host}`)
     const vscodeUri = workspaceFolder.uri.with({
       fragment: url.hash,
-      path: url.pathname,
+      path: Uri.joinPath(workspaceFolder.uri, url.pathname).path,
       query: url.search,
     })
 
@@ -35,6 +35,10 @@ export const createWorkspaceProxyServer = async (
         .header('Last-Modified', new Date(fileStat.mtime).toUTCString())
 
       if (!(fileStat.type & FileType.Directory)) {
+        console.debug(
+          `[Proxy request]: ${req.url} -> ${vscodeUri.toString()} (200)`
+        )
+
         res
           .status(200)
           .send(Buffer.from(await workspace.fs.readFile(vscodeUri)))
@@ -42,9 +46,12 @@ export const createWorkspaceProxyServer = async (
         return
       }
     } catch (e) {
-      // fallback
+      console.warn(e)
     }
 
+    console.debug(
+      `[Proxy request]: ${req.url} -> ${vscodeUri.toString()} (404)`
+    )
     res.status(404).send('Not found')
   })
 


Test patch:
diff --git a/src/workspace-proxy-server.test.ts b/src/workspace-proxy-server.test.ts
index 8ac80d8..05e1d97 100644
--- a/src/workspace-proxy-server.test.ts
+++ b/src/workspace-proxy-server.test.ts
@@ -12,13 +12,13 @@ describe('Workspace Proxy Server', () => {
   let server: WorkspaceProxyServer | undefined
 
   const { fetch } = fetchPonyfill()
-  const wsUri: Uri = Object.assign(Uri.parse('untitled:untitled'), {
-    with: jest.fn(() => wsUri),
-  })
+  const wsUri: Uri = Uri.file('/test/path/subdir')
   const wsFolder: any = { uri: wsUri }
 
   beforeEach(() => {
-    ;(wsUri.with as any).mockReset()
+    jest.spyOn(console, 'debug').mockImplementation()
+    jest.spyOn(console, 'warn').mockImplementation()
+
     server = undefined
   })
 
@@ -29,6 +29,8 @@ describe('Workspace Proxy Server', () => {
       .spyOn(workspace.fs, 'readFile')
       .mockResolvedValue(textEncoder.encode('readFile'))
 
+    const wsUriWithSpy = jest.spyOn(wsUri, 'with')
+
     server = await createWorkspaceProxyServer(wsFolder)
     expect(server.port).toBeGreaterThanOrEqual(8192)
 
@@ -39,8 +41,8 @@ describe('Workspace Proxy Server', () => {
     expect(await response.text()).toMatchInlineSnapshot(`"readFile"`)
 
     expect(response.headers.get('content-type')).toContain('image/png')
-    expect(wsUri.with).toHaveBeenCalledWith({
-      path: '/test.png',
+    expect(wsUriWithSpy).toHaveBeenCalledWith({
+      path: '/test/path/subdir/test.png',
       query: '?query',
       fragment: '',
     })