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 CSS injection in development #2282

Closed
tjk opened this issue Feb 26, 2021 · 12 comments
Closed

SSR CSS injection in development #2282

tjk opened this issue Feb 26, 2021 · 12 comments

Comments

@tjk
Copy link
Contributor

tjk commented Feb 26, 2021

Relevant previous issues / discussion:

User-land vue-specific patch that (partially, see NOTE below) works for me (haven't seen how it hydrates yet) --

/* relevant snippet from server.js */
const cssUrls = new Set(), cssJsUrls = new Set()
function collectCssUrls(mod) {
  mod.importedModules.forEach(submod => {
    if (submod.id.match(/\?vue.*&lang\.css/)) return cssJsUrls.add(submod.url)
    if (submod.file.endsWith(".css")) return cssUrls.add(submod.url)
    if (submod.file.endsWith(".vue")) return collectCssUrls(submod)
    /* TODO include js files like routes that include other components */
    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') /* TODO 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)

Curious about latest thoughts on this!

NOTE: It looks like there is some non-determinism to the above... sometimes css modules are missing...

@egoist
Copy link
Contributor

egoist commented Feb 27, 2021

I just inlined the css as <style> tags in the HTML:

const collectCssUrls = (mods: Set<ModuleNode>, styles: Map<string, string>) => {
  for (const mod of mods) {
    if (mod.ssrModule && mod.file && mod.id) {
      if (mod.file.endsWith('.css') || /\?vue&type=style/.test(mod.id)) {
        styles.set(mod.url, mod.ssrModule.default)
      }
    }
    if (mod.importedModules.size > 0) {
      collectCssUrls(mod.importedModules, styles)
    }
  }
}

Some changes need to be made in updateStyle in order to make hydration work, or at least expose sheetsMap and leave that to users:

const sheetsMap = new Map()

@tjk
Copy link
Contributor Author

tjk commented Mar 1, 2021

Your method is great! It seems like some sfc style modules might be determined lazily and so aren't in the module graph on first load (but then exist on subsequent ones)?

@egoist
Copy link
Contributor

egoist commented Mar 2, 2021

Your method is great! It seems like some sfc style modules might be determined lazily and so aren't in the module graph on first load (but then exist on subsequent ones)?

Yeah, you should call collectCssUrls when the async components are also loaded, like calling it after serverRenderer.renderToString

@tjk
Copy link
Contributor Author

tjk commented Mar 2, 2021

That makes sense. I moved it down shortly after writing that last comment and don't think I've seen the intermittent partial loads since. Looking forward to this behavior being supported internally! (okay to keep this issue open to track that effort?)

@brillout
Copy link
Contributor

This is how vite-plugin-ssr solves this: see ssrEnv.viteDevServer.moduleGraph.getModuleByUrl(filePath) and collectCss() in https:/brillout/vite-plugin-ssr/blob/9e0b1771baa0234aef0ea10605e4a48702806bd3/src/getPreloadTags.node.ts. (Latest version at https:/brillout/vite-plugin-ssr/blob/master/src/getPreloadTags.node.ts (possibly 404)).

@SasanFarrokh
Copy link

This could be fixed with this vite plugin:

module.exports = {
    enforce: 'post',
    apply: 'serve',
    transform (code, id, ssr) {
        if (ssr && id.endsWith('.css')) {
            return `global.css = (global.css || []).concat("${code.trim().slice(16, -1)}")`;
        }
    },
}

and then in server.js

const html = template.replace('<!--HEAD-->', `<style>${global.css.join('\n')}</style>`)

@tjk
Copy link
Contributor Author

tjk commented May 25, 2021

It looks like both of our approaches stopped working somewhere between 2.3.0 and 2.3.4 and it's not immediately obvious from the CHANGELOG. Investigating...

(mod.ssrModule.default and code are just empty strings now instead of being the css code)

@Niputi
Copy link
Contributor

Niputi commented May 25, 2021

I'm guessing you would be looking for this change 65d333d

@tjk
Copy link
Contributor Author

tjk commented May 25, 2021

Yes was just about to edit my comment (after scanning the diff). :)

Do you have any ideas / suggestions on how to proceed?

@ferdinando-ferreira
Copy link
Contributor

ferdinando-ferreira commented May 25, 2021

Do you have any ideas / suggestions on how to proceed?

I posted a working implementation on the other issue. Both the attempts from this issue and the alternative I proposed use artifacts of the internal build process, which leads to instability.

Here is a way to test

git clone https:/ferdinando-ferreira/vite.git --single-branch -b before-merge before-merge
cd before-merge
yarn
yarn build
cd packages/playground/ssr-vue
yarn dev

After that open localhost:3000, the html will have the styles embedded.

It is a small change from ssr-vue from the playground of this very repository, the minimal test case to show how to do it, here are the changes from the vanilla ssr-vue: ferdinando-ferreira@e70bd50

@fallemand
Copy link
Contributor

fallemand commented Mar 26, 2022

Working solution with Hydration.
Based on the previous solutions, this can be used to have hydration.
We inject each css module with a particular id, and then we use the Vite HMR api to listen if a component has been updated, and we remove that particular css.

// server
import { componentsModules, collectCss } from "./collect-css-ssr";
// You have to identify the vue components to render. In our case we use `router.matchedComponents` from `vue-router`.
// But if you have a single entry for you app, just point to your app.vue file.
const componentsToRender = router.getMatchedComponents();
const componentsPath = componentsToRender.map((component) => component.options.__file);
const html = renderer.renderToString(app, context);
const matchedModules = componentsModules(componentsPath, vite);
const css = collectCss(matchedModules);
return html.replace("<!--dev-ssr-css-->", css);

Then in the client, you have to listen for component updates, and remove the CSS from the header.

// client-entry.ts
import { removeCssHotReloaded } from "./collect-css-ssr";
removeCssHotReloaded();

Util collect-css-ssr.ts

// collect-css-ssr.ts
import { ViteDevServer, ModuleNode, UpdatePayload } from "vite";

/**
 * Collect SSR CSS for Vite
*/
export const componentsModules = (components: string[], vite: ViteDevServer) => {
  const matchedModules = new Set<ModuleNode>();
  components.forEach((component) => {
    const modules = vite.moduleGraph.getModulesByFile(component);
    modules?.forEach((mod) => matchedModules.add(mod));
  });
  return matchedModules;
};

export const collectCss = (
  mods: Set<ModuleNode>,
  styles = new Map<string, string>(),
  checkedComponents = new Set(),
) => {
  for (const mod of mods) {
    if (
      (mod.file?.endsWith(".scss") ||
        mod.file?.endsWith(".css") ||
        mod.id?.includes("vue&type=style")) &&
      mod.ssrModule
    ) {
      styles.set(mod.url, mod.ssrModule.default);
    }
    if (mod.importedModules.size > 0 && !checkedComponents.has(mod.id)) {
      checkedComponents.add(mod.id);
      collectCss(mod.importedModules, styles, checkedComponents);
    }
  }
  let result = "";
  styles.forEach((content, id) => {
    const styleTag = `<style type="text/css" vite-module-id="${hashCode(id)}">${content}</style>`;
    result = result.concat(styleTag);
  });
  return result;
};

/**
 * Client listener to detect updated modules through HMR, and remove the initial styled attached to the head
 */
export const removeCssHotReloaded = () => {
  if (import.meta.hot) {
    import.meta.hot.on("vite:beforeUpdate", (module: UpdatePayload) => {
      module.updates.forEach((update) => {
        const moduleStyle = document.querySelector(
          `[vite-module-id="${hashCode(update.acceptedPath)}"]`,
        );
        if (moduleStyle) {
          moduleStyle.remove();
        }
      });
    });
  }
};

const hashCode = (moduleId: string) => {
  let hash = 0,
    i,
    chr;
  if (moduleId.length === 0) return hash;
  for (i = 0; i < moduleId.length; i++) {
    chr = moduleId.charCodeAt(i);
    hash = (hash << 5) - hash + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return hash;
};

@patak-dev
Copy link
Member

Closing this issue as there isn't a concrete change to be applied to Vite in it. @tjk or others that are interested, please create a Discussion with a summary and a link back to this issue if you would like to keep this thread open (not moving the issue to a discussion automatically as it would destroy the layout).

@github-actions github-actions bot locked and limited conversation to collaborators May 17, 2022
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

9 participants