vite: Missing/broken sourcemaps for JS modules w/ imports when used with Vue

Describe the bug

I originally reported this to JetBrains, and they seem to think it’s an issue caused by sourcemaps not being generated by Vite. The same project moved over, running, and debugging on Vue-CLI does not have this issue and breakpoints are hit in the correct file.

https://youtrack.jetbrains.com/issue/WEB-55544

IntelliJ/WebStorm/VSCode seems to be unable to properly debug local JavaScript files when debugging JS modules imported into Vue components in a Vite project.

If you import a regular JS module into your component which itself has an import IJ/WS/VSC is unable to properly map the local .js file to the remote file for JavaScript debugging purposes due to missing/broken sourcemaps. When you place a breakpoint in your JS module source a new, read-only copy of the remote file is loaded from http://localhost:3000/src/xxx/xxx.js in the IDE and the breakpoint stops there instead of in the original JS file. This means that you have to debug in a read-only copy and then swap to your local copy for edits, which can be a big pain.

It looks like this only happens when the modules themselves have imports. You can see that the remote file has a changed “import” line and so that might be causing it to not match up. Here’s a screenshot from a sample project where the import inside the module causes the mismatch. You can see this difference between the local and “remote” on line 1.

I’ve also created an example project which replicates this issue.

https://github.com/Smef/vite-debug-issue-demo

Screen Shot 2022-04-15 at 3 04 34 PM

Reproduction

https://github.com/Smef/vite-debug-issue-demo

System Info

System:
    OS: macOS 12.3.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 147.20 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.14.0 - ~/.nvm/versions/node/v16.14.0/bin/node
    npm: 8.3.1 - ~/.nvm/versions/node/v16.14.0/bin/npm
  Browsers:
    Chrome: 100.0.4896.88
    Firefox: 99.0.1
    Safari: 15.4
  npmPackages:
    @vitejs/plugin-vue: ^2.3.1 => 2.3.1 
    vite: ^2.9.5 => 2.9.5

Used Package Manager

npm

Logs

No response

Validations

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 6
  • Comments: 32 (20 by maintainers)

Commits related to this issue

Most upvoted comments

I’ve did a quick check and it seems like to be much better than before with few caveats:

There’s still a problem with proper mapping to the exact source file for rewritten .js files. What does it mean, is that the IDE cannot properly identify which source file corresponds to the one seen by the browser. What is better that at least with the present source map the IDE can apply some heuristics to try to identify the related source file, which may or may not work well.

  • Let’s say I have a file like: vite-root/src/foo/bar.js. The sourcemap embedded in the file will have the information "sources":["/src/foo/bar.js"].
  • But when we compare it with another file: vite-root/src/foo/baz.ts, the sourcemap in this file will have this: "sources":["baz.ts"].

Now I cannot argue which one is better, but what I can say for sure that both cannot be good, and also what I can say is that IntelliJ IDE is able to identify baz.ts correctly all the time, while it cannot reliably identify bar.js.

Surely enough, if a simple project when there’s only one file that matches /src/foo/bar.js path segment, IntelliJ will - I assume applying some heuristics - deduce that it must be the right file. But if there’s another file that is matching the path segment /src/foo/bar.js the IDE may choose that file for the debugging display. This can easily happen in a monorepo + workspaces setup when some packages are visible for the IDE under both the source path and both under node_modules/packagename/src/foo/bar.js. It is also possible to have multiple different folders opened in the ide with similar paths, e.g.: vite-root/src/foo/bar.js, some-other-folder/src/foo/bar.js or even vite-root/another-folder/src/foo/bar.js. In these cases the IDE will show the debugger with the first file it finds, which may very well be a completely wrong file.

This suggests that the "sources":["/src/foo/bar.js"] in the sourcemap is not exact. The IDE can find the file based on it when there’s only one match, but may miss it if there are multiple. If we compare it with the TypeScript behaviour, where "sources":["baz.ts"] is in the sourcemap, we can see that is working properly all the time, no matter how many other places contain baz.ts file. Based on this I think the sources in the sourcemap should be relative to the path where the sourcemap is (or the file the sourcemap is embedded into). This would explain why a bare baz.ts works perfectly all the time, but /src/foo/bar.js creates ambiguity.

Another strange thing I’ve noticed is that the view the browser (both Chrome and Firefox) reconstructs for the original file contains the last line //# sourceMappingURL=data:application/json;base64,... while this is not the case for .ts files. This would not affect debugging I think, as this is an additional line at the end which noone could set breakpoints against anyway, but still it is a notable difference compared to “normal” (i.e. conventional) behaviour. I guess the reson for this is that the sourcemap of .ts files contain a sourcesContent section, so the browsers would use that, while the sourcemap for the .js files does not have this, only the mappings section, which allows the browser to reconstruct (albeit probably with additional computation) the parts of the original source corresponding to the modified file served by Vite, but it cannot reconstruct the last //# sourceMappingURL=... line into anything in the original source, so it is just left there as it is at the end. But apparently this difference is ignored by IntelliJ IDE, again I assume based on a heuristics that says ignore inlined sourcemaps when comparing the file seen by the editor and the file contents sent by the browser. At the end of the day having sourcesContent in these maps may be preferable if no other reasons present to omit that part - I know there were performance concerns about full-blown sourcemaps. But I think adding the sourcesContent would be a negligible cost compared to generating the mappings section, as the sourcesContent is basically just inlining of the source file already loaded by Vite.

I seem to be having this issue after migrating from CRA. I came here from Jetbrain Youtrack Issue. VScode hits breakpoints that return a remote copy of the file instead of the expected actual source file in the VScode editor.

So additional findings:

  • The propsed fix vitejs/vite#9476 broke tests in SvelteKit as can be seen here: https://github.com/sveltejs/kit/pull/7543
  • The propsed fix vitejs/vite#9476 has been unmerged
  • This issue is still an issue. (Should be reopened)
  • The problem is that by adding the dummy sourcemap some files which did not have any sourcemaps till now suddenly got one, however since the added sourcemaps were generated with the MagicString config highres: false it only included line numbers, which broke the tests mentioned above, because the test suit expected the columns to be correct as well. This is because that file in the test was not rewritten by Vite, so no sourcemap meant exact match was possible, but the non-high-res sourcemap made the mapping worse.
  • Obvious solution would be to implement high-res sourcemaps.
  • High res sourcemaps were not implemented in vitejs/vite#9476 because of assumptions about bad performance impact.
  • During investigation we also found that importAnalysis plugin tracks changes during dev in a MagicString instance, just skips the sourcemap generation at the end. This means it would be better to generate the sourcemap there instead of the workaround dummy sourcemap generation.

Now the plan is this (thx @dominikg):

  1. revert PR vitejs/vite#9476 to restore the behaviour we know (see here: vitejs/vite#11144)
  2. update the original issue with the additional information (see this comment)
  3. start a new PR, add 2 testcases that a) check stacktrace positions with line and column and b) sourcemap accuracy in general. b would initially fail due to importanalysis updates not being recorded
  4. implement proper sourcemap support for importanalysis in dev (test should pass now)
  5. test perf impact by comparing main with that PR
  6. if 5 shows low to no impact, just change the behavior, if it shows slowdowns, add an option

Regarding adding an option, obviously bloating the config with options is not a goal, but the current situation actually hurts both of 2 possible usecases:

  • Someone with high sensitivity to performance and willing to give up sourcemaps cannot do it, and most assets still include sourcemaps also MagicString is used by importAnalysis adding even more performance impact, even thought the map is not generated there. This all slows down their serve without any use.
  • Someone who needs the maps and willing to accept the performance penalty is almost good, except for JS assets, which is a huge blow for those working on non-TypeScript projects.

Obviously, the benefit of giving up sourcemaps would need more changes to transformation plugins, as all should than observe this configuration and skip using MagicString and generating sourcemaps, but it could potentially speed up dev mode a lot when sourcemaps are not needed.

So now I’ll make another PR with a more comprehensive fix and do some tests there to see if performance is really a concern to address with a new setting, or can we just enable sourcemaps for JS importAnalysis and be happy with that.

EDIT: We have switched to TypeScript where this is basically a no issue, so I’ll eventually not got to the point of working on it any further.

If this works, I think we only need to inject sourcemap when a query is injected (example: ?t=) and sourcemap is not generated by anything.

Sourcemap always have to be injected when there is a change made to any JS files, because otherwise the IDE cannot properly present the debugger. The initial request from the browser for a JS file imported into a Vue component (my test case) is without any query string. Of course, if I make a change and hot reload gets a new version of the file, a ?t=... query will be attached, but mapping to the original source file is needed also for the initially loaded file.

So the cases I see are:

1.) Contents of a file are not changed at all by Vite, just served as they are: no need for sourcemap, because the IDE will know based on path the correct file, and when the IDE compares the file seen by the Browser to the file it has will recognise they are the same - it is an assumption on my side, but this explains the behaviour I see, namely that if the file is edited in a way, that Vite does not have to make changes to it, the debugger works fine even without a sourcemap.

2.) Contents of a file are changed by Vite, but the changes are only rewritten imports: no need for an actual sourcemap, but needs a dummy sourcemap with an empty mappings, names etc, only having a one element sources array pointing at the original file. This will only work properly as long as no new lines are inserted or no lines are removed from the file. Here we can utilise the fact that the imports are fine to be different in the IDE and the browser as an import cannot be stepped into, and no other lines of the code are changed therefore when the IDE steps in the code it can show the proper line and position.

3.) Contents of a file are changed by Vite beyond only rewritten imports: In this case a full blown sourcemap is required for proper debugging. Without a sourcemap the IDE would show a read only copy of the served file (original issue) and if a minimal sourcemap is used as in 2. the IDE would show breakpoints at incorrect lines and character positions.

Now I’m not sure if Vite would ever do a type 3. change to a file during serving it in dev mode. As long as it is only 1. or 2. and we add the minimalistic sourcemap with only a single element source array pointing to the original source file, it should be okay. But in this case MagicString is not needed at all, simple string manipulation could be sufficient.

I can confirm it is working at least in my test case as shown above, with only setting the source in the map, without generating any mappings.

By the same token, if the overhead associated with generating an actual SourceMap is not welcome in dev mode (understandable), maybe importAnalysisPlugin should use just a regular string instead of a MagicString, as why to bear the overhead of collecting the changes if we don’t use them at the end anyway? (I don’t know actually how the overhead of using MagicString to manipulate the source vs. generating the proper map that includes mappings compares. It might be the case that the former is negligible overhead, but I don’t know. Was it tested/examined, or it may only be an illusion that not generating a map at the end solves the overhead, if prepend(), overwrite() etc calls to MagicString are also significantly slower than just splitting and concatenating standard srings. Not sure…)