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

SSR HTML transformation in middleware mode doesn't add style tags for statically imported css #2013

Closed
3 tasks done
airhorns opened this issue Feb 13, 2021 · 10 comments
Closed
3 tasks done

Comments

@airhorns
Copy link
Contributor

airhorns commented Feb 13, 2021

  • Read the docs.
  • Use Vite >=2.0. (1.x is no longer supported)
  • If the issue is related to 1.x -> 2.0 upgrade, read the Migration Guide first.

Describe the bug

During development, when using vite in middleware mode like the ssr-react example, CSS source files imported by the entrypoint aren't sourced via a <link/> tag in the transformed HTML. This results in a flash of unstyled content because the server rendered response doesn't include the CSS, but during hydration, the client renders and imports the style.

I believe that in normal, vite is the server serving the HTML mode, these static imports get parsed out and written into the HTML file served to the user.

Reproduction

You can try out the branch at main...airhorns:ssr-static-assets, which just adds a static import for a CSS file to the App.tsx entrypoint in the ssr-react playground.

The server rendered HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
<script type="module" src="/@vite/client"></script>
  <script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"><nav><ul><li><a href="/about">About</a></li><li><a href="/">Home</a></li></ul></nav><h1>Home</h1></div>
    <script type="module" src="/src/entry-client.jsx"></script>
  </body>
</html>

The DOM tree after rendering client side:

<html lang="en"><head>
<script type="module" src="/@vite/client"></script>
  <script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  <style type="text/css">body {
  background-color: red;
}
</style></head>
  <body>
    <div id="app"><nav><ul><li><a href="/about">About</a></li><li><a href="/">Home</a></li></ul></nav><h1>Home</h1></div>
    <script type="module" src="/src/entry-client.jsx"></script>
</body></html>

Note the added inline script tag with the background-color: red;

It'd be great for vite to support this same transform when doing transformIndexHtml, but also, if there was a way to get the list of imports necessary for sticking into the HTML when doing ssrLoadModule, that'd be awesome too! If you build you can use manifest: true or ssrManifest: true to get enough information out of vite to create the right HTML file, but, that's only after building, so if we wanted to support people doing this during development, it'd be great to have an API for getting the list of static imports that an ssrLoadModule should demand.

Thanks for an awesome piece of software!

System Info

  • vite version: 2a6109a
  • Operating System: Darwin inspector 20.2.0 Darwin Kernel Version 20.2.0: Wed Dec 2 20:39:59 PST 2020; root:xnu-7195.60.75~1/RELEASE_X86_64 x86_64
  • Node version: v15.7.0
  • Package manager (npm/yarn/pnpm) and version: yarn 1.22.10
@yyx990803
Copy link
Member

That's expected, imported CSS are dynamically injected at runtime during dev.

@airhorns
Copy link
Contributor Author

Just to clarify, is it possible to fix the flash of unstyled content and it’s just deemed to be too much work, or is there some fundamental limitation that makes dev work differently than the build such that it can’t really be fixed?

@airhorns
Copy link
Contributor Author

And, could we add an API so dev time integrations could serve the linked assets from a module within SSR’d HTML if they wanted to?

@tjk
Copy link
Contributor

tjk commented Feb 25, 2021

Curious about this as well. Is the limitation at least documented somewhere? (I guessed it's sort of covered on https://ssr.vuejs.org/guide/css.html#css-management)

@brillout
Copy link
Contributor

@tjk
Copy link
Contributor

tjk commented Feb 26, 2021

Got a userland patch done quickly with the guidance from @brillout linked discussion. Don't think I will spend time on more polished version for internals right now but maybe this can help someone in the meantime.

/* relevant snippet from server.js */
const cssUrls = [], cssJsUrls = []
function collectCssUrls(mod) {
  mod.importedModules.forEach(submod => {
    if (submod.id.match(/\?vue.*&lang\.css/)) return cssJsUrls.push(submod.url)
    if (submod.file.endsWith(".css")) return cssUrls.push(submod.url)
    if (submod.file.endsWith(".vue")) return collectCssUrls(submod)
    // XXX need to continue recursing in your routes file
    if (submod.file.match(/route/)) return collectCssUrls(submod)
  })
}
let render
if (!isProd) {
  render = (await vite.ssrLoadModule("/src/entry-server.js")).render
  const mod = await vite.moduleGraph.getModuleByUrl('/src/app.js') /* replace with your entry */
  cssUrls = mod.ssrTransformResult.deps.filter(d => d.endsWith(".css"))
} else {
  render = require("./dist/server/entry-server.js").render
}
const [appHtml] = await render(url, manifest)
const devCss = cssUrls.map(url => {
  return `<link rel="stylesheet" type="text/css" href="${url}">`
}).join("") + cssJsUrls.map(url => {
  return `<script type="module" src="${url}"></script>`
}).join("")
const html = template.replace(`<!--app-html-->`, appHtml).replace(`<!--dev-css-->`, devCss)

Older version missing the SFC style blocks:

/* relevant snippet from server.js */
let cssUrls = []
let render
if (!isProd) {
  render = (await vite.ssrLoadModule("/src/entry-server.js")).render
  const mod = await vite.moduleGraph.getModuleByUrl('/src/app.js') /* replace with your entry */
  cssUrls = mod.ssrTransformResult.deps.filter(d => d.endsWith(".css"))
} else {
  render = require("./dist/server/entry-server.js").render
}
const [appHtml] = await render(url, manifest)
const css = cssUrls.map(url => `<link rel="stylesheet" type="text/css" href="${url}">`).join("")
const html = template.replace(`<!--app-html-->`, appHtml).replace(`<!--dev-css-->`, css)

@brillout
Copy link
Contributor

Vue tracks the loaded CSS in the context object, see the ssr-vue example.

@tjk
Copy link
Contributor

tjk commented Feb 26, 2021

The renderToString second parameter context object? I don't think I see css in there...

You are talking about here? https:/vitejs/vite/blob/main/packages/playground/ssr-vue/src/entry-server.js#L16

@tjk
Copy link
Contributor

tjk commented Feb 26, 2021

PS: My updated comment above now includes the SFC style blocks. Probably very suboptimal but looks like it works decently for dev for now.

@brillout
Copy link
Contributor

@tjk Yes I exactly meant that ctx object. Maybe it only works in prod, since I guess the CSS is extracted in prod.

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

No branches or pull requests

4 participants