Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix some issues with preprocess source maps #5754

Merged
merged 11 commits into from
Dec 16, 2020

Conversation

dmitrage
Copy link
Contributor

@dmitrage dmitrage commented Dec 8, 2020

Hi! I was trying to recalculate locations of warnings using sourcemap generated from preprocess and I think that I've found some issues. Here is the list of changes I came up to:

  1. Set source of preprocessed file as basename (relative to itself - "from" and "to" is the same file in case of preprocess - also fixed weird paths like src/src/Input.svelte after remapping)
  2. Apply offset only to segments pointing at component source file
  3. Offset segments with first original line, not generated one
  4. Align count of generated sourcemap lines with count of code lines in generated code (should fix Sourcemaps offset svelte-preprocess#286)
  5. Compute last line length before mutating source
  6. Replace if(seg[N]) with more explicit length checks to avoid false positives on zero indexes
  7. Treat returned content without map as not changed

I would appreciate feedback on this. Pinging @milahu and @dummdidumm based on recent activity on linked issue but everyone is welcome :) Thanks!

Copy link
Member

@dummdidumm dummdidumm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you!

The basename-stuff got me thinking. Maybe we need another test to check that external sources behave as expected. So for example that this

<style lang="scss" src="./someexternal.scss"></style>

<h1>Hello world</h1>

Is mapped correctly. So for example if there's a warning inside the transpiled css that there's no original position (because it does not exist inside the original Svelte file) and that the subsquent code is not affected, either.

test/sourcemaps/samples/typescript/test.js Outdated Show resolved Hide resolved
return decoded_map.sources.findIndex(source => !source);
}
// different tools produce different sources
// (file name, relative path, absolute path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a test for these different cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added guess-source. I'm not sure if this is the right approach though... While it may workaround some cases, maybe it would be better to require correct maps from preprocessors?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think it would be a good idea to have preprocessors producing more consistent sourcemaps either way even if we didn't require them here. Requiring them sounds like a reasonable idea though. Do you know which ones are producing these different paths and what the preferred version is? It could be helpful to file an issue against https://github.com/sveltejs/svelte-preprocess with these details

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had issue with postcss transformer. The first thing that comes to mind - it should pass "to" parameter to postcss (equal to "from"). Will check and report later.

@dmitrage
Copy link
Contributor Author

dmitrage commented Dec 9, 2020

Removed source guessing.
Also discovered that (at least) css map generation is affected by a bug that is fixed in latest version of magic-string (extra segment is generated for removed chunk). Should I bump magic-string in this branch?

@dummdidumm
Copy link
Member

dummdidumm commented Dec 9, 2020

I think we can bump magic-string, thoughts @Conduitry ?

What was the reason to remove source guessing?

@dmitrage
Copy link
Contributor Author

dmitrage commented Dec 9, 2020

What was the reason to remove source guessing?

The more I looked at it, the more wrong this feature seemed to me.

For example, if there were two sources src/dep.smth and src/input.svelte, in current form they would be transformed to src/dep.smth and input.svelte (wrong). We could attempt to update other sources based on diff from first change, but it becomes hacky, unreliable and complicated.

I think there shouldn't be such workaround here and it's up to preprocessors to produce correct sources.

@benmccann
Copy link
Member

Yeah, I would say to go ahead and bump magic-string

@dmitrage
Copy link
Contributor Author

dmitrage commented Dec 9, 2020

Resulted in failed compile-option-dev test :) Tried to fast-fix it by porting to new utility method instead of hardcoded constants, but it did not help. Will investigate later today.

@benmccann
Copy link
Member

If upgrading magic-string is going to be difficult then perhaps we should do it in a new PR to keep this one from growing too large

Copy link
Member

@benmccann benmccann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice you have this marked as Draft. Anything you wanted to do before reviewing/merging? It looks good to me

@milahu
Copy link
Contributor

milahu commented Dec 9, 2020

@dmitrage
your function sourcemap_add_offset is broken and slow, see my comments

but seems like politeness is better than competence ..
at least some other dev will have fun fixing your bug

edit: my bad, i was too dumb to actually release my pending comments

@dummdidumm
Copy link
Member

Insults don't help. And what comments where?

Copy link
Contributor

@milahu milahu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review go! (can you now see my comments?)

src/compiler/utils/string_with_sourcemap.ts Outdated Show resolved Hide resolved
src/compiler/utils/string_with_sourcemap.ts Outdated Show resolved Hide resolved
src/compiler/utils/string_with_sourcemap.ts Outdated Show resolved Hide resolved
src/compiler/utils/string_with_sourcemap.ts Outdated Show resolved Hide resolved
src/compiler/utils/string_with_sourcemap.ts Show resolved Hide resolved
src/compiler/preprocess/index.ts Outdated Show resolved Hide resolved
@dmitrage
Copy link
Contributor Author

I notice you have this marked as Draft. Anything you wanted to do before reviewing/merging? It looks good to me

I want to ensure that all changes are correct, maybe add some new tests. Run some benchmarks on real code. And cleanup a bit.

I also want to finish with TODO in PR description and maybe bump magic-string. Will move to another PR if solution is not trivial enough.

@dmitrage
Copy link
Contributor Author

Sorry for the delay and blocking others, had less spare time than expected. I decided to postpone some things so ready for final review.

I renamed concat test to sourcemap-concat and removed script-and-style because there is already preprocessed-multiple.

Notes about postponed:

  1. svelte.compile output should be relative to sources (after combining preprocess map and svelte maps)
    Case: svelte.compile from src/input.svelte to dst/output.js + dst/output.css
    preprocess sourcemap sources: ['input.svelte', 'external.css'] (I used sourcemap-offsets test for example)
    svelte sourcemap sources for js: ['../src/input.svelte']
    svelte sourcemap sources for css: ['input.svelte'] (1)
    After combine:
    expected: ['../src/input.svelte', '../src/external.css']
    actual: ['input.svelte', 'external.css'] (2)
    We need to go deeper (-_-):
  • (1) option.cssOutputFilename is never used. I guess it should be passed to stylesheet.render instead of option.filename (with fallback to it) when options.css != true.
  • (2) Combination of source maps is mostly done by remapping module. Need to investigate what can be done to get the desired behavior. Maybe we should also pay some attention to file and sourceRoot fields.
  1. magic-string bump:
    Let's start with inspecting generated source map for css output of test compile-options-dev.
    Version 0.25.3 generates incorrect duplicate sequential segments like 40->7:24 (wrong) and 40->8:1 (correct). While 0.25.7 does not have same, I've found strange behavior in overwrite function in both versions when replacement is multiline. Segment is added only to the first generated line, it can even be wrong if replacement starts with \n. This is why test still failed after I ported it to helper function. So, bumping magic-string deserves more careful testing and should be done separately.

  2. There are still some edge cases waiting to be covered with tests

I won't be able to work on these in few days (not related to cbp2077 release as one could thought :-P) and will file issues a bit later so that somebody could pick it.

@dmitrage dmitrage marked this pull request as ready for review December 11, 2020 11:43
Copy link
Member

@dummdidumm dummdidumm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes! It's good that we iterate towards a sound solution.

Copy link
Member

@benmccann benmccann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. @milahu did you want to take any last look at this before we merge it?

Comment on lines -30 to +31
if (seg[2]) seg[2] += offset.line;
// shift only segments that belong to component source file
if (seg[1] === source_index) { // also ensures that seg.length >= 4
// shift column if it points at the first line
if (seg[2] === 0) {
seg[3] += offset.column;
}
// shift line
seg[2] += offset.line;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to have a test for "only shift lines of one source file"
probably you have good reasons for this change, but its not yet transparent

if your solution is right, then we get another problem:
what about basename collisions?
a/index.svelte + b/index.svelte --> c/index.svelte

the sourcemap.names array is not sorted, so its not predictable
so there is no (simple) way to choose the right index.svelte file
lazy solution: print a warning.
"dear user, sorry, but our compiler knows files only by their basename .."

a more radical solution: instead of file names, use file paths.
these are always relative to the project root
all our plugins would have to preserve these paths

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test for "only shift lines of one source file"

sourcemap-offsets should cover this. Or am i misunderstanding you?

what about basename collisions?

I think that I have a different vision of the build pipeline:

  1. Preprocess
  • We assume that preprocess src === dst and map file is placed in same dir. We use basename for component in sources since it should be relative to the sourcemap file.
  • We use basename for chunks that we don't pass to preprocessors (StringWithSourcemap.from_source)
  • We pass full filename to preprocessors and expect sources to contain basename when referring component being preprocessed and relative to filename paths for external inclusions. For example, expect ["index.svelte", "../b/index.svelte"] when transforming a/index.svelte (no collision here).
  1. Compile
  • We pass filename, outputFilename and cssOutputFilename. Based on these, svelte compiler creates source map with sources as relative paths.
  • Then we combine js and css maps from compiler with preprocessed map so that segments point at locations in original source code and sources in resulted maps are relative from output map file path to original source files.
  1. Bundle (optional)
  • Should work as usual since we input compiled files with maps pointing directly at original sources.

Is there anything missing, you disagree with or that I should describe more clearly? I'd prefer not to land something cryptic or/and broken :)

P.S. Just out of interest, is it synthetic or do you know setups when preprocess step combines multiple components into one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourcemap-offsets should cover this.

yes! sorry. ("dear mila, please just read the source")

We use basename for component in sources since it should be relative to the sourcemap file.

yes .. i wrongly assumed that the basename-function is applied to the preprocessor-result
while its only applied to the svelte.preprocess input filename

We pass full filename to preprocessors and expect sources to contain basename when referring component being preprocessed

this is my problem with this solution ..
why do we always convert file paths to basenames?
(always, without giving an option to the user)

short answer: for consistency with the existing svelte.compile

good news: i am happy with your patch : )

long answer:

[email protected] + [email protected]
svelte.preprocess and svelte.compile receive relative paths
options = { filename: 'src/App.svelte' }

[email protected] + [email protected]
svelte.preprocess and svelte.compile receive absolute paths
options = { filename: '/home/user/project/src/App.svelte' }

why do we always convert file paths to basenames?

the actual reason here is MagicString, who turns file paths into basenames:

// svelte/src/compiler/compile/render_dom/index.ts
const css = component.stylesheet.render(
  options.filename, // file path
  !options.customElement
);

// svelte/src/compiler/compile/css/Stylesheet.ts
// Stylesheet.render
return {
  code: code.toString(),
  map: code.generateMap({
    includeContent: true,
    source: this.filename, // file path
    file
  })
};
// --> result.map.sources: basename

// magic-string/src/MagicString.js
// MagicString.generateDecodedMap
return {

  // path to basename
  file: options.file ? options.file.split(/[/\\]/).pop() : null,

  sources: [options.source ? getRelativePath(options.file || '', options.source) : null],
  sourcesContent: options.includeContent ? [this.original] : [null],
  names,
  mappings: mappings.raw
};

so the path-to-basename conversion is done for consistency with other sourcemaps, generated by svelte.compile

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we always convert file paths to basenames?

basename is just a result of computing relative path from file to itself since I assumed that we "put" results of preprocess into the same directory where source code lives.

If someone needs more control we could add additional option outputFilename to preprocess so that specific relative paths are generated instead. For example, take ./src/index.svelte, preprocess it and put into ./preprocessed/index.svelte, and then pass those files to svelte.compile independently. In this case, instead index.svelte (basename) we would have our component referred by index.svelte.map as ../src/index.svelte (relative to map file location - from {cwd}/preprocessed/index.svelte.map to {cwd}/src/index.svelte).

But I'm not sure that such feature is really needed and worth complication both in usage and implementation since usually preprocess is done in-memory and can be treated as atomic operation with compile. I might be wrong here though. But then it can be implemented separately.

So I would say it's more about simplicity, not consistency.

Copy link
Contributor

@milahu milahu Dec 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a test for the basename conversion? something like ....

import { magic_string_bundle } from '../../helpers';

const external_filename = 'external_code.css';
export const external_code = `/* external code start */

span {
	--external_code-var: 1px;
}

/* external code end */`;

const component_filepath = 'src/input.svelte';
const component_basename = 'input.svelte';

export default {
	js_map_sources: [component_basename],
	css_map_sources: [component_basename, external_filename],
	preprocess: [
		{
			style: ({ content, filename }) => {
				return magic_string_bundle([
					{ code: external_code, filename: external_filename },
					{ code: content, filename }
				]);
			}
		}
	],
	options: {
		filename: component_filepath
	}
};

it can be implemented separately.

yes .. low priority stuff

what i meant was:
optionally, svelte should "simply" preserve all file paths (relative or absolute)
then its the bundler's job (rollup, webpack) to feed pretty file paths to svelte

for example

src/App.svelte should be preserved

when src/App.svelte includes ./style/included.css
then its resolved to src/style/included.css

makes easier the debugging of larger projects with many files + folders

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes easier the debugging of larger projects with many files + folders

Sorry, will response ASAP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@milahu I think debugging should be fine. Bundler (or dev server) gives us filename, usually absolute or relative to cwd. We return compiled code and source map with sources relative to given filename (as they are stored in map). I think it's up to bundler/server to correctly resolve sources in resulting map. I tried some simple cases with rollup and it seemed to work as expected. Would appreciate if you could help me finding some repro with broken debugging experience so I could play with it if you have some time :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well thats easy ..
https://github.com/milahu/svelte-template--reproduce-basename-vs-filepath

actual result:
all paths are reduced to basename
(see my other comment, this is done by MagicString.generateDecodedMap)

expeted result:
optionally show the full file path, for example src/menu/App.svelte

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the filename relative to cwd in rollup-plugin-svelte in sveltejs/rollup-plugin-svelte#131 because that's what I had seen in the couple example source maps I looked at. Assuming that's the correct behavior (I haven't fully followed the full discussion here), we could make that change to webpack's svelte-loader as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cloned it, launched and console logged the same. I also did some tweak after, will describe in my next comment. But if I hover App.svelte:2, I see full url to correct source file. Clicking also navigates to where I would expect.

@dmitrage
Copy link
Contributor Author

While investigating https://github.com/milahu/svelte-template--reproduce-basename-vs-filepath I made some changes:

  • convert to TS using included script
  • tweak some configs to force source map on TS
  • fork svelte plugin, use compiler from this branch, pass map from preprocessed to svelte.compile sourcemap

Next build and... INCORRECT MAPPINGS... After some investigation I located that the problem was in generating not-mapped source maps for cases when preprocessor returns code without map (it covered N-1 lines). Fixed, but now the messages are reported from the bundle itself (I mean in devtools console) - as expected since code without map from preprocessor is treated as not mapped externally taken from nowhere.

One thing that could be tweaked - check if code returned from preprocessor equals code passed to it and there is no source map (i.e. nothing changed). But not sure about this - preprocessor can return undefined.

@milahu
Copy link
Contributor

milahu commented Dec 13, 2020

now the messages are reported from the bundle itself (I mean in devtools console) - as expected since code without map from preprocessor is treated as not mapped externally taken from nowhere.

One thing that could be tweaked - check if code returned from preprocessor equals code passed to it and there is no source map (i.e. nothing changed).

are these two connected?

how is that behavior expected?
when i have <script>console.log('this is src/menu/App.svelte')</script> in src/menu/App.svelte
then the console should show App.svelte or src/menu/App.svelte, and not bundle.js, no?

@dmitrage
Copy link
Contributor Author

Original template: no preprocess, rollup generates sourcemap from sources, console.log calls are mapped to original components.
Modified template: preprocess returns code without map, console.log calls are mapped to zones without original sources.

P.S. Looked at svelte-preprocess and found that it has many places returning {code: content}. So I think we should treat is as not modified.

@milahu
Copy link
Contributor

milahu commented Dec 13, 2020

console.log calls are mapped to zones without original sources.

this means that remapping could not trace back the mappings

see StringWithSourcemap.from_source to generate an identity map
(thats the quickfix, maybe there is a better way)

edit: in StringWithSourcemap.from_source we have the same old "shift columns in first line" logic

// shift columns in first line
const segment_list = map.mappings[0];
for (let segment = 0; segment < segment_list.length; segment++) {
  segment_list[segment][3] += offset.column;
}

not sure if thats correct, a test would be nice ..

@dmitrage
Copy link
Contributor Author

this means that remapping could not trace back the mappings

We pass preprocessed result to StringWithSourcemap.from_processed and it generates map with empty lines when sourcemap is missing.

we have the same old "shift columns in first line" logic

Will re-check but since we have one-to-one mapping I expect it to work (i.e. only and all segments on first generated line are pointing at segments on first original line).

@benmccann
Copy link
Member

@dmitrage @milahu let me know when you guys are satisfied with this PR and I can merge. I wasn't sure if everything's wrapped up or there's more you'd like to do still

@dmitrage
Copy link
Contributor Author

dmitrage commented Dec 16, 2020

Right now I have no plans to change anything here.

But there is #5793 and let me talk about it a bit.

To get initially scared, I ran some preprocess tools with URL fake file path on win32.

TL;DR:

  • TypeScript has basename in sources
  • PostCSS has URL
  • Sass has something ... unexpected (mix of URL and resolved cwd)
console.log(
  require('typescript').transpileModule('123', {
    fileName: 'file:///somewhere/file.ts',
    outFile: 'file:///somewhere/file.js',
    compilerOptions: { sourceMap: true }
  }).sourceMapText
);

// => {"version":3,"file":"file.js","sourceRoot":"","sources":["file.ts"],"names":[],"mappings":"AAAA,GAAG,CAAA"}

console.log(
  require('postcss')()
    .process('div {}', {
      from: 'file:///somewhere/file.pcss',
      to: 'file:///somewhere/file.css',
      map: {
        inline: false
      }
    })
    .map.toString()
);

// => {"version":3,"sources":["file:///somewhere/file.pcss"],"names":[],"mappings":"AAAA,KAAK","file":"file:///somewhere/file.css","sourcesContent":["div {}"]}

console.log(
  require('sass')
    .renderSync({
      file: 'file:///somewhere/file.scss',
      outFile: 'file:///somewhere/file.css',
      data: 'div { color: red; }',
      sourceMap: true,
      sourceMapEmbed: false
    })
    .map.toString()
);

// => {"version":3,"sourceRoot":"","sources":["file:///D:/DM/Development/Local/check-tools/file:/somewhere/file.scss"],"names":[],"mappings":"AAAA;EAAM","file":"file.css"}

We pass filename as is to preprocessor and expect component file_basename in sources to match it. We need this match to apply offsets correctly. And it works fine with tools above when using non-URL absolute/relative file paths. But as we can see, some new tweaks are required for URL case and I don't have a ready answer where exactly.

We could try something like:

  • Use filename instead file_basename in case of URL
  • Match using both filename and file_basename
  • Apply some normalizations to results of preprocessors
  • Pre-resolve URL to file path when passing to preprocessors
  • Release a book "Crafting Svelte preprocessors. From beginner to pro in 21 days" Design and document strict requirements for preprocessors.

... And there could be multiple external sources returned by preprocessor... Should they also have file:// form at this step?

So it quickly gets too out of scope of current PR. This one is mostly about offsets in preprocessed source code. I think that a dedicated PR with focus on correctly computing sources in both preprocess and compile steps would be a better place to make decisions.

Edit: read discussion again and maybe there is no need to care about passing URL to preprocessors at all.

@milahu
Copy link
Contributor

milahu commented Dec 16, 2020

@dmitrage @milahu let me know when you guys are satisfied with this PR and I can merge.

im happy

But there is #5793 and let me talk about it a bit.

lets continue there, tiss one's long-e-nuff

@benmccann benmccann merged commit 68538c6 into sveltejs:master Dec 16, 2020
@benmccann
Copy link
Member

The SASS one seems too crazy for us to deal with it and we should probably file a bug against it and try to get it fixed at the source

@benmccann
Copy link
Member

Btw, if you guys have any more interest in carrying on this work, I saw a couple TODOs in the code where CSS source maps are not fully implemented

map: null // TODO

// TODO concatenate CSS maps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sourcemaps offset
4 participants