vue icon indicating copy to clipboard operation
vue copied to clipboard

Generate distinct paths for supporting .vue files in sourcemaps

Open connor4312 opened this issue 4 years ago β€’ 21 comments

What problem does this feature solve?

Hello Vue πŸ‘‹

I'm working on our new JavaScript debugger for VS Code, and ran into some problems when trying out Vue. The default vue-cli setup creates several supporting .vue scripts. For instance, open the Chrome devtools and ctrl p for "App.vue" in a new project. The top entry is the correct Vue file, the others aren't.

From the VS Code perspective, the user might ask us to set a breakpoint in a Vue file. We know which file the breakpoint is attached to on disk, and we need to figure out which loaded file that breakpoint gets mapped to. However, because all these paths are quite similar and all (in the context of a generic web app) possibly the file we want, we end up putting the breakpoint in the wrong file and breaking in the incorrect place. And often the supporting scripts evaluate before the 'real' script does, so we can't, for example, wait and pick the best match from the possible candidates.

What does the proposed API look like?

The simplest solution would be to prefix the supporting files with some path like __vue__, or something along those lines. This would prevent the paths from incorrectly mapping to files that exist on disk.

connor4312 avatar Jan 17 '20 19:01 connor4312

Maybe @znck or @sodatea know more about where to do something here

posva avatar Jan 18 '20 15:01 posva

This happens in vue-loader, right now I don’t have bandwidth to take it but I can guide if someone is interested.

znck avatar Jan 18 '20 16:01 znck

I could probably find some time to help out. Code pointers or guidance would be appreciated πŸ™‚

connor4312 avatar Jan 18 '20 16:01 connor4312

This is turning out to be a little more tricky than I thought. Convincing webpack to resolve paths that don't actually exist on the filesystem is difficult; I've made some headway with the virtual-module-webpack-plugin, though it's somewhat dangerous and there's more work to do to have imports from adjusted modules resolve correctly, I'm not really happy with this solution. Maybe there's a way to tweak change the locations in the sourcemaps for these files without needing to ask Webpack to treat them as entirely different disk files.

I've pushed some ugly scratch work to https://github.com/connor4312/vue-loader

connor4312 avatar Jan 23 '20 18:01 connor4312

In https://github.com/microsoft/vscode-js-debug/commit/2f3c93a08df0ceb09eda86251d0309d34bfb26ef I introduced a heuristic that seems to work very well for plain Vue with JS files, where the path is different (as in the screenshot above). However, with lang="ts" in vue components the path of the generated code is entirely indistinguishable from that of the generated code.

E.g., only one of these is the right one:

Without doing some kind of source code analysis, which is complex and fragile, I can't deterministically tell which one is the 'right' script to map back to the file.

connor4312 avatar Jul 12 '20 03:07 connor4312

Would be neat to see this fixed as well. It's difficult to know which one is correct even as a human looking at Chrome Devtools :shrug:

sirlancelot avatar Jul 13 '20 05:07 sirlancelot

@znck let me know what you think a good solution is. We've gotten continued reports of people running into this with VS Code πŸ™‚

connor4312 avatar Sep 13 '20 15:09 connor4312

Looking forward to the fix

ariyuan avatar Sep 24 '20 07:09 ariyuan

It's been almost 10 months with no indication of it being fixed...

jtsom avatar Sep 24 '20 17:09 jtsom

@znck I would appreciate your input here. This continues to be a pain point for Vue users on VS Code. (most recently in https://github.com/microsoft/vscode-js-debug/issues/754)

connor4312 avatar Oct 30 '20 21:10 connor4312

It is quite tricky to make source maps and hot reload work simultaneously. I understand it's a huge pain point and I'll revisit it (hopefully coming weekend).

znck avatar Nov 02 '20 13:11 znck

Awesome, let me know if I can help with anything

connor4312 avatar Nov 02 '20 15:11 connor4312

@znck @connor4312 Any movement on this issue? I can provide a project in early days that you can debug against

JoeEarly avatar Feb 06 '21 17:02 JoeEarly

From the VS Code perspective, the user might ask us to set a breakpoint in a Vue file. We know which file the breakpoint is attached to on disk, and we need to figure out which loaded file that breakpoint gets mapped to. However, because all these paths are quite similar and all (in the context of a generic web app) possibly the file we want, we end up putting the breakpoint in the wrong file and breaking in the incorrect place. And often the supporting scripts evaluate before the 'real' script does, so we can't, for example, wait and pick the best match from the possible candidates.

I've found a workaround which is to provide a custom function to webpack's output.devtoolModuleFilenameTemplate property which names component files according to which loaders are in use, i.e. if it is a vue file, the query is type=script and there are no additional loaders (e.g. ts-loader), then simply name the file Component.vue and breakpoints set in vscode work.

The following is only tested with vue-loader@16 in a typescript without babel project:

output: {
  devtoolModuleFilenameTemplate: info => {
    if (info.allLoaders === '') {
      // when allLoaders is an empty string the file is the original source
      // file and will be prefixed with src:// to provide separation from
      // modules transpiled via webpack
      const filenameParts = ['src://']
      if (info.namespace) {
        filenameParts.push(info.namespace + '/')
      }
      filenameParts.push(info.resourcePath.replace(/^\.\//, ''))
      return filenameParts.join('')
    } else {
      // otherwise we have a webpack module
      const filenameParts = ['webpack://']
      if (info.namespace) {
        filenameParts.push(info.namespace + '/')
      }
      filenameParts.push(info.resourcePath.replace(/^\.\//, ''))
      const isVueScript = info.resourcePath.match(/\.vue$/) &&
        info.query.match(/\btype=script\b/) &&
        !info.allLoaders.match(/\bts-loader\b/)
      if (!isVueScript) {
        filenameParts.push('?' + info.hash)
      }
      return filenameParts.join('')
    }
  },
}

This assumes that a .vue file will only ever have one <script></script> element.

Additionally, for vue-loader@16 (vue 3 users), an extra step is required so that source maps have the correct line numbering (due to the way newline handling prior to <script></script> is handled. @vue/compiler-sfc requires pad: true but this isn't exposed anywhere by vue-loader so the function must be overridden until that functionality is added, i.e.

const CompilerSfc = require('@vue/compiler-sfc')
const parse = CompilerSfc.parse
CompilerSfc.parse = (source, options) => {
  return parse(source, Object.assign({ pad: true }, options))
}

I explain the reasoning behind the latter in a bit more detail in https://github.com/vuejs/vue-cli/issues/2897#issuecomment-788952677.

With those 2 changes, I'm able to set breakpoints in vscode and have execution pause at the correct place, including in async functions and timer callbacks, e.g. setTimeout and setInterval.

I've created a minimal ts + vue 3 project to test with: https://github.com/andrewmackrodt/vue3-ide-breakpoint-test

andrewmackrodt avatar Mar 02 '21 18:03 andrewmackrodt

@andrewmackrodt Thank you!

For anyone using vscode's debugger with this solution, you should also remember that to update the sourceMapPathOverrides in their launch config correspondingly. As such:

      "sourceMapPathOverrides": {
        "webpack://src/*": "${webRoot}/src/*",
        "src://src/*": "${webRoot}/src/*"
      }

1two3code avatar Mar 20 '21 18:03 1two3code

@znck let me know what you think a good solution is. We've gotten continued reports of people running into this with VS Code and I remain more than happy to help get this fixed πŸ™‚

connor4312 avatar Dec 29 '21 16:12 connor4312

Beware that this code

const CompilerSfc = require('@vue/compiler-sfc')
const parse = CompilerSfc.parse
CompilerSfc.parse = (source, options) => {
  return parse(source, Object.assign({ pad: true }, options))
}

leads to huge bugs with latest versions of vue-loader (currently 16.8.3, 17.0.0, maybe a bit earlier). The most amazing is completely wrong source parsing, when even your variables names are splitted into several parts and then webpack complains partial_var_name is unknown :). But maybe it just me

shameleo avatar Dec 30 '21 06:12 shameleo

Well, spend 4 hours trying to resolve this vue-sfc-debug-typescript issue. Finally only this helped https://github.com/vuejs/vue/issues/11023#issuecomment-789137369 In order to initially land breakpoint properly you also need to have:

      "sourceMapPathOverrides": {
        "webpack://src/*": "${webRoot}/*"
      }

@andrewmackrodt you are genius, respect and many thanks for work done!

uka17 avatar Jan 24 '22 23:01 uka17

Thanks to everybody here, especially @andrewmackrodt -- I owe you a beer (or beverage of your choice). Your output.devtoolModuleFilenameTemplate did the trick. (I didn't need the @vue/compiler-sfcfix since I'm not using Vue 3).

Just a few notes to help anybody else in my position. I'm working on a containerized project with Vue 2, manually configured (i.e. without vue-cli), using docker-compose and HMR. Also:

We have one repo, with the frontend in a subdirectory of the project root, and a package.json in that subdirectory, so something like this:

project
β”œβ”€β”€ backend-name
└── frontend-package-name
    β”œβ”€β”€ package.json
    └── src
        β”œβ”€β”€ App.vue
        └── components

When generating pathnames in the source map, vue-loader uses the package name (I think) at the root. After including the output.devtoolModuleFilenameTemplate, my source map pathnames look like this:

src://frontend-package-name/src/App.vue

As a result, my path override is just:

      "sourceMapPathOverrides": { 
        "src://*": "${webRoot}/*"
      }

I recommend right-clicking on a filename in Chrome devtools (in the Sources tab's directory tree) and using Copy link address to see what you need for your sourceMapPathOverrides. Without this information I would have been lost.

Also, my vscode debug config is setup to attach to an already running chrome, launched from the command line with the --remote-debugging-port=9222. My launch.json is then:

    {
      "name": "Attach to Chrome",
      "port": 9222,
      "url": "http://localhost/*",
      "request": "attach",
      "type": "pwa-chrome",
      "sourceMapPathOverrides": {
        "src://*": "${webRoot}/*"
      }
    },

One final note: My webpack devtool is set to eval-cheap-module-source-map. This is probably something you don't have to worry about when you're using vue-cli, but I thought I'd mention it.

deisner avatar Mar 17 '22 18:03 deisner

Hello All, I've a project in vue 2.6.14, and I made it work only fully removing the propery name from package.json and package-lock.json; There's a question i answered in stackoverflow with details

dev-vinicius-andrade avatar Aug 05 '22 21:08 dev-vinicius-andrade

Connor Peet indicated I was experiencing this issue when he researched why I cannot set breakpoints in VS Code for my Vue3 app.

[https://github.com/microsoft/vscode-js-debug/issues/1799]

I just wanted to sound off here and say I spent many hours over several days trying to get breakpoint setting to work in VS Code. It gave me a pretty negative impression of the IDE. I'm glad I stuck with it and submitted the issue to you guys and learned that VS Code did indeed have an issue.

I subsequently have migrated my project to Vite and as a result, no longer have the breakpoint setting issue. Apparently Vue3 with Vite does not suffer from the same issue. It took a few hours to migrate my project but it was worth it. Vite is fast!

John

johnlwebb3 avatar Sep 14 '23 13:09 johnlwebb3