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: don't duplicate styles with dynamic import (fix #9967) #9970

Merged
merged 4 commits into from
Sep 23, 2022
Merged

fix: don't duplicate styles with dynamic import (fix #9967) #9970

merged 4 commits into from
Sep 23, 2022

Conversation

bgoscinski
Copy link
Contributor

Description

Fixes #9967 by normalizing asset URLs to full absolute URLs with origin and comparing them to link.href properties of all links in the document to find duplicates.

Additional context


What is the purpose of this pull request?

  • Bug fix
  • New Feature
  • Documentation update
  • Other

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines and follow the Commit Convention.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

@bgoscinski
Copy link
Contributor Author

@patak-dev This is ready for review :)

@patak-dev
Copy link
Member

I don't think we should modify the way paths work when the base is not relative. You can think about it as an optimization, and there may already be plugins that expect a certain pattern for Vite preload paths. The bug only appears for relative base, so let's fix it for it.

@bgoscinski
Copy link
Contributor Author

@patak-dev ready for next round :)

  • added back relativePreloadUrls and different code for relative and absolute base
  • added comments clarifying the logic of isPreloaded

@bgoscinski
Copy link
Contributor Author

@Tal500 @patak-dev I added test from this PR on top of Tal500's work here https://github.com/bgoscinski/vite/tree/preload-on-system-format-with-9967-test. Unfortunately the test fails :(
image

@Tal500
Copy link
Contributor

Tal500 commented Sep 7, 2022

@Tal500 @patak-dev I added test from this PR on top of Tal500's work here https://github.com/bgoscinski/vite/tree/preload-on-system-format-with-9967-test. Unfortunately the test fails :( image

Hi! As I said in my comment on you code review, I know that my code isn't perfect (intentionally, because of failing CI) on avoiding duplicated CSS.
The purpose of my PR is different - the partial CSS duplication is only a "side business".
The reason my code isn't fully avoid duplication, is because not both sides of the path equality test are always absolute, and I "cheated" there to avoid failing CI. (Hope you'd find why the CI are failing, assuming some tests are wrong)

The review I sent you was not related to the fact I fail to avoid some duplication.
I was just saying that you can take the idea of instead passing through all the link in the page, per dep, is just do CSS query that takes the relevant links that ends with the same file name, when you trim the parent dir from the URL.
Then, after you have a small set of candidates(e.g. with the hashes it will almost always be exactly one candidate), then check the absolute paths of both sides of the path equation. This just saves so much time for performance, and it's not a too complicated idea.

@bgoscinski
Copy link
Contributor Author

@Tal500 Ahhhh I see! I assumed that your PR is a "superset" of mine and it fixes the bug. That's why I ported my test to your PR :)

In this case I'll borrow the trick with the CSS selector :)

Tal500
Tal500 previously approved these changes Sep 7, 2022
Copy link
Contributor

@Tal500 Tal500 left a comment

Choose a reason for hiding this comment

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

Great!

packages/vite/src/node/plugins/importAnalysisBuild.ts Outdated Show resolved Hide resolved
Tal500 added a commit to Tal500/vite that referenced this pull request Sep 10, 2022
@bgoscinski
Copy link
Contributor Author

bgoscinski commented Sep 12, 2022

@patak-dev ready for next pass :) I don't know why CI failed on MacOS but I think this is some kind of a flakiness issue.

Changes summary:

When relativePreloadUrls is false (base isn't relative), then we don't have a bug, no? So we can leave the code as it was before. Or is there a bug here too? IIUC, the bug you reported only happens for relative base.

Addressed. Please see isBaseRelative.

And in this case [base is relative], what we need is to compare new URL(link.href, location.href).href === dep

As I pointed out here and demonstrated here link.href must be absolute (with protocol, host and all the other URL parts). This behavior is also specified here under "If the content attribute is defined to contain a URL". With this in mind new URL(link.href, location.href).href is IMO redundant. If you could please share a scenario that fails without new URL(link.href, location.href).href I'll be happy to add a test for it and implement required changes to make it pass.

I think the test cases may not be correctly testing the issue you reported because CI is passing, or maybe I'm missing something here.

I run my project with Vite built from this branch and bug is gone so I think we're good.

@Tal500
Copy link
Contributor

Tal500 commented Sep 12, 2022

@patak-dev ready for next pass :) I don't know why CI failed on MacOS but I think this is some kind of a flakiness issue.

Changes summary:

When relativePreloadUrls is false (base isn't relative), then we don't have a bug, no? So we can leave the code as it was before. Or is there a bug here too? IIUC, the bug you reported only happens for relative base.

Addressed. Please see isBaseRelative.

And in this case [base is relative], what we need is to compare new URL(link.href, location.href).href === dep

As I pointed out here and demonstrated here link.href must be absolute (with protocol, host and all the other URL parts). This behavior is also specified here under "If the content attribute is defined to contain a URL". With this in mind new URL(link.href, location.href).href is IMO redundant. If you could please share a scenario that fails without new URL(link.href, location.href).href I'll be happy to add a test for it and implement required changes to make it pass.

I think the test cases may not be correctly testing the issue you reported because CI is passing, or maybe I'm missing something here.

I run my project with Vite built from this branch and bug is gone so I think we're good.

Don't you think that the best, simple thing is to always compare their absolute URLs, no matter what the configuration is?
It's also future safe.

@bgoscinski
Copy link
Contributor Author

@Tal500

Don't you think that the best, simple thing is to always compare their absolute URLs, no matter what the configuration is? It's also future safe.

That's what I initially implemented but @patak-dev suggested to postpone it at least until Vite 4

patak-dev
patak-dev previously approved these changes Sep 12, 2022
packages/vite/src/node/plugins/importAnalysisBuild.ts Outdated Show resolved Hide resolved
@@ -75,10 +83,17 @@ function preload(
seen[dep] = true
Copy link
Member

Choose a reason for hiding this comment

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

This is just an idea but maybe we could simplify the logic if we use new URL(dep, document.baseURI).href. If the SSR markup used an absolute path, I guess the current approach won't work. (it didn't work from before though)

Copy link
Contributor

Choose a reason for hiding this comment

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

that was what I missed! To supply the base url!
Earlier I used new URL(dep).href that didn't make sense when dep is relative, just understand it now, according to MDN about the constructor signature new URL(url, base):

If url is a relative URL, base is required, and will be used as the base URL. If url is an absolute URL, a given base will be ignored.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is just an idea but maybe we could simplify the logic if we use new URL(dep, document.baseURI).href. If the SSR markup used an absolute path, I guess the current approach won't work. (it didn't work from before though)

NOOOOOOO! BaseURI isn't supported on IE11!

Copy link
Contributor

Choose a reason for hiding this comment

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

This is just an idea but maybe we could simplify the logic if we use new URL(dep, document.baseURI).href. If the SSR markup used an absolute path, I guess the current approach won't work. (it didn't work from before though)

NOOOOOOO! BaseURI isn't supported on IE11!

OK, since this PR doesn't introduce legacy support, I think it's fine to use BaseURI.
It seems that a correct place to handle this issue is at my PR #9920, since right now the preload function isn't called for legacy browsers.
Therefore, @bgoscinski, you may ignore the rest of this comment, I'll handle with it after in my PR.

The full solution was introduced in sapper legacy support, on PR sveltejs/sapper#1562.
I'll explain briefly the logic of this sapper PR, how it handle the situation when document.baseURI isn't available:
We could have used the document.URL which is supported by IE11. Sadly, due to the reasons in this stackoverflow answer, if there is some <base> element in the DOM, it changes the base path, ignoring the current URL(demo is included in the stackoverflow answer). Luckily, by the specifications, there could be only one at most such <base> element, so we just need to grab the one we see, and take this to be the baseURI. If no such tag were found, we can simply take document.URL instead.

@bgoscinski
Copy link
Contributor Author

@sapphi-red Ready for next round.

I fixed the bug with <link rel="preload" as="style" href="foo.css"> and modified the tests to make sure this won't regress in the future. Also I didn't quite get what you meant here. Could you please clarify? Also is this something you want to see implemented in this PR?

Aaaand macos build&test failed again... Do you have an idea what might be the cause?

/cc @patak-dev @Tal500

@Tal500
Copy link
Contributor

Tal500 commented Sep 15, 2022

BTW: Too bad you always squash&force-push the commits to the PR branch. I think it's better for history and the conversation to not do this. Don't worry, when the Vite team merge the PR, it will be squashed, as you can see in the contributing guidelines:

It's OK to have multiple small commits as you work on the PR. GitHub can automatically squash them before merging.

Comment on lines +80 to 96
const isBaseRelative = !!importerUrl

// check if the file is already preloaded by SSR markup
if (isBaseRelative) {
// When isBaseRelative is true then we have `importerUrl` and `dep` is
// already converted to an absolute URL by the `assetsURL` function
for (let i = links.length - 1; i >= 0; i--) {
const link = links[i]
// The `links[i].href` is an absolute URL thanks to browser doing the work
// for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5
if (link.href === dep && (!isCss || link.rel === 'stylesheet')) {
return
}
}
} else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe @sapphi-red suggestion here was to do my initial thought, just in a way that actually fix my issue?
As far as I can tell, it omits the if (isBaseRelative), and the suggestion is to do this:

Suggested change
const isBaseRelative = !!importerUrl
// check if the file is already preloaded by SSR markup
if (isBaseRelative) {
// When isBaseRelative is true then we have `importerUrl` and `dep` is
// already converted to an absolute URL by the `assetsURL` function
for (let i = links.length - 1; i >= 0; i--) {
const link = links[i]
// The `links[i].href` is an absolute URL thanks to browser doing the work
// for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5
if (link.href === dep && (!isCss || link.rel === 'stylesheet')) {
return
}
}
} else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
return
}
const absoluteDep = new URL(dep, document.baseURI).href;
for (let i = links.length - 1; i >= 0; i--) {
const link = links[i]
// The `links[i].href` is an absolute URL thanks to browser doing the work
// for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5
if (link.href === absoluteDep && (!isCss || link.rel === 'stylesheet')) {
return
}
}

@bgoscinski
Copy link
Contributor Author

BTW: Too bad you always squash&force-push the commits to the PR branch. I think it's better for history and the conversation to not do this. Don't worry, when the Vite team merge the PR, it will be squashed, as you can see in the contributing guidelines:

It's OK to have multiple small commits as you work on the PR. GitHub can automatically squash them before merging.

I figured that out some time ago .

@sapphi-red
Copy link
Member

I fixed the bug with <link rel="preload" as="style" href="foo.css"> and modified the tests to make sure this won't regress in the future.

Thanks!

Also I didn't quite get what you meant here. Could you please clarify?

When an HTML rendered by server used an absolute path for CSS, the CSS will be duplicated.

For example, when document.baseURI is https://example.com/ and the server responded with:

<html>
<head>
  <link rel="stylesheet" href="https://example.com/assets/foo.hash.css">
  <script type="module" src="https://example.com/assets/index.hash.js"></script>
</head>
</html>

__preload will inject <link rel="stylesheet" href="/assets/foo.hash.css">.
But it shouldn't inject it because <link rel="stylesheet" href="https://example.com/assets/foo.hash.css"> already exists.

My idea is basically same to #9970 (comment). (I guess we could use seen variable.)

Also is this something you want to see implemented in this PR?

It's a related to the original bug, but this one isn't a bug caused by this PR. So this could be fixed in a different PR. It's not blocking this PR.

@bgoscinski
Copy link
Contributor Author

@sapphi-red Thanks for clarifying! What else is needed from my side to get this PR ready to merge?

Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

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

Thanks! LGTM

@patak-dev patak-dev added p3-minor-bug An edge case that only affects very specific usage (priority) feat: css labels Sep 18, 2022
@patak-dev patak-dev added this to the 3.2 milestone Sep 18, 2022
Tal500 added a commit to Tal500/vite that referenced this pull request Sep 18, 2022
The reason is that it's already fixed by vitejs#9970, and we want to make this PR independent.
@patak-dev patak-dev merged commit 65f97bd into vitejs:main Sep 23, 2022
@sapphi-red sapphi-red mentioned this pull request Oct 26, 2022
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: css p3-minor-bug An edge case that only affects very specific usage (priority)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Relative base option + dynamic import leads to styles injected two times in production build
4 participants