Git Product home page Git Product logo

vite-ssr's Introduction

Vite SSR logo

Vite SSR

Simple yet powerful Server Side Rendering for Vite 2 in Node.js (Vue & React).

  • ⚑ Lightning Fast HMR (powered by Vite, even in SSR mode).
  • πŸ’β€β™‚οΈ Consistent DX experience abstracting most of the SSR complexity.
  • πŸ” Small library, unopinionated about your page routing and API logic.
  • πŸ”₯ Fast and SEO friendly thanks to SSR, with SPA takeover for snappy UX.
  • 🧱 Compatible with Vite's plugin ecosystem such as file-based routing, PWA, etc.

Vite SSR can be deployed to any Node.js or browser-like environment, including serverless platforms like Vercel, Netlify, or even Cloudflare Workers. It can also run with more traditional servers like Express.js or Fastify.

Vite SSR is unopinionated about your API logic so you must bring your own. If you want a more opiniated and fullstack setup with filesystem-based API endpoints and auto-managed edge cache, have a look at Vitedge. It wraps Vite SSR and can be deployed to Cloudflare Workers or any Node.js environment.

Start a new SSR project right away with filesystem routes, i18n, icons, markdown and more with Vitesse (Vue) or Reactesse (React). See live demo.

Installation

Create a normal Vite project for Vue or React.

yarn create vite --template [vue|vue-ts|react|react-ts]

Then, add vite-ssr with your package manager (direct dependency) and your framework router.

# For Vue
yarn add vite-ssr vue@3 vue-router@4 @vueuse/head@^0.9.0

# For React
yarn add vite-ssr react@16 react-router-dom@5

Make sure that index.html contains a root element with id app: <div id="app"></div> (or change the default container id in plugin options: options.containerId).

Usage

Add Vite SSR plugin to your Vite config file (see vite.config.js for a full example).

// vite.config.js
import vue from '@vitejs/plugin-vue'
import viteSSR from 'vite-ssr/plugin.js'
// import react from '@vitejs/plugin-react'

export default {
  plugins: [
    viteSSR(),
    vue(), // react()
  ],
}

Then, simply import the main Vite SSR handler in your main entry file as follows. See full examples for Vue and React.

import App from './App' // Vue or React main app
import routes from './routes'
import viteSSR from 'vite-ssr'
// or from 'vite-ssr/vue' or 'vite-ssr/react', which slightly improves typings

export default viteSSR(App, { routes }, (context) => {
  /* Vite SSR main hook for custom logic */
  /* const { app, router, initialState, ... } = context */
})

That's right, in Vite SSR there's only 1 single entry file by default πŸŽ‰. It will take care of providing your code with the right environment.

If you need conditional logic that should only run in either client or server, use Vite's import.meta.env.SSR boolean variable and the tree-shaking will do the rest.

The third argument is Vite SSR's main hook, which runs only once at the start. It receives the SSR context and can be used to initialize the app or setup anything like state management or other plugins. See an example of Vue + Pinia here. In React, the same SSR Context is passed to the main App function/component as props.

Available options

The previous handler accepts the following options as its second argument:

  • routes: Array of routes, according to each framework's router (see vue-router or react-router-config).
  • base: Function that returns a string with the router base. Can be useful for i18n routes or when the app is not deployed at the domain root.
  • routerOptions: Additional router options like scrollBehavior in vue-router.
  • transformState: Modify the state to be serialized or deserialized. See State serialization for more information.
  • pageProps.passToPage: Whether each route's initialState should be automatically passed to the page components as props.
  • styleCollector: Only in React. Mechanism to extract CSS in JS. See integrations#React-CSS-inJS.
  • debug.mount: Pass false to prevent mounting the app in the client. You will need to do this manually on your own but it's useful to see differences between SSR and hydration.

SSR Context

The context passed to the main hook (and to React's root component) contains:

  • initialState: Object that can be mutated during SSR to save any data to be serialized. This same object and data can be read in the browser.
  • url: Initial URL.
  • isClient: Boolean similar to import.meta.env.SSR. Unlike the latter, isClient does not trigger tree shaking.
  • request: Available during SSR.
  • redirect: Isomorphic function to redirect to a different URL.
  • writeResponse: Function to add status or headers to the response object (only in backend).
  • router: Router instance in Vue, and a custom router in React to access the routes and page components.
  • app: App instance, only in Vue.
  • initialRoute: Initial Route object, only in Vue.

This context can also be accesed from any component by using useContext hook:

import { useContext } from 'vite-ssr'

//...
function() {
  // In a component
  const { initialState, redirect } = useContext()
  // ...
}

Using separate entry files

Even though Vite SSR uses 1 single entry file by default, thus abstracting complexity from your app, you can still have separate entry files for client and server if you need more flexibility. This can happen when building a library on top of Vite SSR, for example.

Simply provide the entry file for the client in index.html (as you would normally do in an SPA) and pass the entry file for the server as a CLI flag: vite-ssr [dev|build] --ssr <path/to/entry-server>.

Then, import the main SSR handlers for the entry files from vite-ssr/vue/entry-client and vite-ssr/vue/entry-server instead. Use vite-ssr/react/* for React.

SSR initial state and data fetching

The SSR initial state is the application data that is serialized as part of the server-rendered HTML for later hydration in the browser. This data is normally gathered using fetch or DB requests from your API code.

Vite SSR initial state consists of a plain JS object that is passed to your application and can be modified at will during SSR. This object will be serialized and later hydrated automatically in the browser, and passed to your app again so you can use it as a data source.

export default viteSSR(App, { routes }, ({ initialState }) => {
  if (import.meta.env.SSR) {
    // Write in server
    initialState.myData = 'DB/API data'
  } else {
    // Read in browser
    console.log(initialState.myData) // => 'DB/API data'
  }

  // Provide the initial state to your stores, components, etc. as you prefer.
})

If you prefer having a solution for data fetching out of the box, have a look at Vitedge. Otherwise, you can implement it as follows:

Initial state in Vue

Vue has multiple ways to provide the initial state to Vite SSR:

  • Calling your API before entering a route (Router's beforeEach or beforeEnter) and populate route.meta.state. Vite SSR will get the first route's state and use it as the SSR initial state. See a full example here.
export default viteSSR(App, { routes }, async ({ app }) => {
  router.beforEach(async (to, from) => {
    if (to.meta.state) {
      return // Already has state
    }

    const response = await fetch('my/api/data/' + to.name)

    // This will modify initialState
    to.meta.state = await response.json()
  })
})
  • Calling your API directly from Vue components using Suspense, and storing the result in the SSR initial state. See a full example with Suspense here. If you prefer Axios, there's also an example here.
import { useContext } from 'vite-ssr'
import { useRoute } from 'vue-router'
import { inject, ref } from 'vue'

// This is a custom hook to fetch data in components
export async function useFetchData(endpoint) {
  const { initialState } = useContext()
  const { name } = useRoute() // this is just a unique key
  const state = ref(initialState[name] || null)

  if (!state.value) {
    state.value = await (await fetch(endpoint)).json()

    if (import.meta.env.SSR) {
      initialState[name] = state.value
    }
  }

  return state
}
// Page Component with Async Setup
export default {
  async setup() {
    const state = await useFetchData('my-api-endpoint')
    return { data }
  },
}

// Use Suspense in your app root
<template>
  <RouterView v-slot="{ Component }">
    <Suspense>
      <component :is="Component" />
    </Suspense>
  </RouterView>
</template>
// Main
export default viteSSR(App, { routes }, ({ app, initialState }) => {
  // You can pass it to your state management
  // or use `useContext()` like in the Suspense example
  const pinia = createPinia()

  // Sync initialState with the store:
  if (import.meta.env.SSR) {
    initialState.pinia = pinia.state.value
  } else {
    pinia.state.value = initialState.pinia
  }

  app.use(pinia)
})

// Page Component with Server Prefetch
export default {
  beforeMount() {
    // In browser
    this.fetchMyData()
  },
  async serverPrefetch() {
    // During SSR
    await this.fetchMyData()
  },
  methods: {
    fetchMyData() {
      const store = useStore()
      if (!store.myData) {
        return fetch('my/api/data').then(res => res.json()).then((myData) => {
          store.myData = myData
        })
      }
    },
  },
}

Initial state in React

There are a few ways to provide initial state in React:

  • Call your API and throw a promise in order to leverage React's Suspense (in both browser and server) anywhere in your components. Vite SSR is already adding Suspense to the root so you don't need to provide it.
function App({ initialState }) {
  if (!initialState.ready) {
    const promise = getPageProps(route).then((state) => {
      Object.assign(initialState, state)
      initialState.ready = true
    })

    // Throw the promise so Suspense can await it
    throw promise
  }

  return <div>{initialState}</div>
}
  • Calling your API before entering a route and populate route.meta.state. Vite SSR will get the first route's state and use it as the SSR initial state. See a full example here.
function App({ router }) {
  // This router is provided by Vite SSR.
  // Use it to render routes and save initial state.

  return (
    <Routes>
      {router.routes.map((route) => {
        if (!route.meta.state) {
          // Call custom API and return a promise
          const promise = getPageProps(route).then((state) => {
            // This is similar to modifying initialState in the previous example
            route.meta.state = state
          })

          // Throw the promise so Suspense can await it
          throw promise
        }

        return (
          <Route key={route.path} path={route.path} element={
            <route.component props={...route.meta.state} />
          } />
        )
      })}
    </Routes>
  )
}

State serialization

Vite SSR simply uses JSON.stringify to serialize the state, escapes certain characters to prevent XSS and saves it in the DOM. This behavior can be overriden by using the transformState hook in case you need to support dates, regexp or function serialization:

import viteSSR from 'vite-ssr'
import App from './app'
import routes from './routes'

export default viteSSR(App, {
  routes,
  transformState(state, defaultTransformer) {
    if (import.meta.env.SSR) {
      // Serialize during SSR by using,
      // for example, using @nuxt/devalue
      return customSerialize(state)

      // -- Or use the defaultTransformer after modifying the state:
      // state.apolloCache = state.apolloCache.extract()
      // return defaultTransformer(state)
    } else {
      // Deserialize in browser
      return customDeserialize(state)
    }
  },
})

Accessing response and request objects

In development, both response and request objects are passed to the main hook during SSR:

export default viteSSR(
  App,
  { routes },
  ({ initialState, request, response }) => {
    // Access request cookies, etc.
  }
)

In production, you control the server so you must pass these objects to the rendering function in order to have them available in the main hook:

import render from './dist/server'

//...

const { html } = await render(url, {
  manifest,
  preload: true,
  request,
  response,
  // Anything here will be available in the main hook.
  initialState: { hello: 'world' }, // Optional prefilled state
})

Beware that, in development, Vite uses plain Node.js + Connect for middleware. Therefore, the request and response objects might differ from your production environment if you use any server framework such as Fastify, Express.js or Polka. If you want to use your own server during development, check Middleware Mode.

Editing Response and redirects

It's possible to set status and headers to the response with writeResponse utility. For redirects, the redirect utility works both in SSR (server redirect) and browser (history push):

import { useContext } from 'vite-ssr'

// In a component
function () {
  const { redirect, writeResponse } = useContext()

  if (/* ... */) {
    redirect('/another-page', 302)
  }

  if (import.meta.env.SSR && /* ... */) {
    writeResponse({
      status: 404,
      headers: {}
    })
  }

  // ...
}

In the browser, this will just behave as a normal Router push.

Head tags and global attributes

Use your framework's utilities to handle head tags and attributes for html and body elements.

Vue Head

Install @vueuse/head as follows:

import { createHead } from '@vueuse/head'

export default viteSSR(App, { routes }, ({ app }) => {
  const head = createHead()
  app.use(head)

  return { head }
})

// In your components:
// import { useHead } from '@vueuse/head'
// ... useHead({ ... })

React Helmet

Use react-helmet-async from your components (similar usage to react-helmet). The provider is already added by Vite SSR.

import { Helmet } from 'react-helmet-async'

// ...
;<>
  <Helmet>
    <html lang="en" />
    <meta charSet="utf-8" />
    <title>Home</title>
    <link rel="canonical" href="http://mysite.com/example" />
  </Helmet>
</>

Rendering only in client/browser

Vite SSR exports ClientOnly component that renders its children only in the browser:

import { ClientOnly } from 'vite-ssr'

//...
;<div>
  <ClientOnly>
    <div>...</div>
  </ClientOnly>
</div>

Development

There are two ways to run the app locally for development:

  • SPA mode: vite dev command runs Vite directly without any SSR.
  • SSR mode: vite-ssr dev command spins up a local SSR server. It supports similar attributes to Vite CLI, e.g. vite-ssr --port 1337 --open.

SPA mode will be slightly faster but the SSR one will have closer behavior to a production environment.

Middleware Mode

If you want to run your own dev server (e.g. Express.js) instead of Vite's default Node + Connect, you can use Vite SSR in middleware mode:

const express = require('express')
const { createSsrServer } = require('vite-ssr/dev')

async function createServer() {
  const app = express()

  // Create vite-ssr server in middleware mode.
  const viteServer = await createSsrServer({
    server: { middlewareMode: 'ssr' },
  })

  // Use vite's connect instance as middleware
  app.use(viteServer.middlewares)

  app.listen(3000)
}

createServer()

Production

Run vite-ssr build for buildling your app. This will create 2 builds (client and server) that you can import and use from your Node backend. See an Express.js example server here, or a serverless function deployed to Vercel here.

Keeping index.html in the client build

In an SSR app, index.html is already embedded in the server build, and is thus removed from the client build in order to prevent serving it by mistake. However, if you would like to keep index.html in the client build (e.g. when using server side routing to selectively use SSR for a subset of routes), you can set build.keepIndexHtml to true in the plugin options:

// vite.config.js

export default {
  plugins: [
    viteSSR({
      build: {
        keepIndexHtml: true,
      },
    }),
    [...]
  ],
}

Integrations

Common integrations will be added here:

React CSS in JS

Use the styleCollector option to specify an SSR style collector. vite-ssr exports 3 common CSS-in-JS integrations: styled-components, material-ui-core-v4 and emotion:

import viteSSR from 'vite-ssr/react'
import styleCollector from 'vite-ssr/react/style-collectors/emotion'

export default viteSSR(App, { routes, styleCollector })

You can provide your own by looking at the implementation of any of the existing collectors.

Note that you still need to install all the required dependencies from these packages (e.g. @emotion/server, @emotion/react and @emotion/cache when using Emotion).

Custom Typings

You can define your own typings with vite-ssr. To declare custom types, the file mostly needs to import or export something not to break other types. Example transforming request and response to types of express:

import { Request, Response } from 'express'

declare module 'vite-ssr/vue' {
  export interface Context {
    request: Request
    response: Response
  }
}

Community contributions

Feel free to submit your projects:

Templates

  • Vue 3, Vercel, Axios. Link.

Addons

  • vite-ssr-middleware: Add route middlewares for vite-ssr and Vue, similar to Nuxt. Link.

Examples

  • Imitating Nuxt's asyncData in Vue options API. Link.
  • Fetch data from Vue components with composition API hook and Axios. Link.
  • Vue + TypeScript with API calls. Link.
  • Vue + TypeScript using serverPrefetch. Link.

References

The following projects served as learning material to develop this tool:

Todos

  • TypeScript
  • Make src/main.js file name configurable
  • Support build options as CLI flags (--ssr entry-file supported)
  • Support React
  • SSR dev-server
  • Make SSR dev-server similar to Vite's dev-server (options, terminal output)
  • Research if vite-ssr CLI logic can be moved to the plugin in Vite 2 to use vite command instead.
  • Docs

vite-ssr's People

Contributors

cabljac avatar cyberap avatar dchenk avatar domsen123 avatar frandiox avatar juvirez avatar kadiryazici avatar littlesound avatar m3hari avatar m4rvr avatar marklyck avatar max-programming avatar posva avatar salqueng avatar techieoriname avatar thomasowow avatar thruthesky avatar ucw avatar webigorkiev avatar xenonym avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

vite-ssr's Issues

ssr-manifest.json should be stored in dist/server

I believe ssr-manifest.json is logically a server dependency and thus should rather be stored in dist/server.

Let's say I want to deploy a SSR server as a micro-service (and publish assets at CDN).
For the micro-service, I will need the entire dist/server folder and for some reason one file from client (which is not used by the client itself). How so?

[v0.10.6] `request` & `response` not available in entry hook when importing from `vite-ssr/vue`

Hey!

After I've upgraded from v0.10.5 to v0.10.6, TypeScript no longer knows that the request & response properties exist in the entry hook.

import viteSSR from 'vite-ssr/vue'

export default viteSSR(
  App,
  {
    routes,
    // This works fine now
    pageProps: {
      passToPage: false,
    },
  },
  // Property `request`/`response` does not exist on type ...
  ({ app, initialState, router, request, response }) => {
    // ...
  }
)

Custom index.html

I have a testing environment that can use either vite or webpack, and to do this I've create a separate index.vite.html and index.webpack.html for each bundler. The following plugin works for vite without SSR, but vite-ssr has index.html hardcoded. I can't actually have a file called index.html or webpack will try to serve it.

{
  name: 'configure-server',
  configureServer(server) {
    return () => {
      server.middlewares.use('/', (req, res, next) => {
        if (req.url === '/index.html') req.url = '/index.vite.html'
        return next()
      })
    }
  }
},

async function getIndexTemplate(url: string) {
// Template should be fresh in every request
const indexHtml = await fs.readFile(resolve('index.html'), 'utf-8')
return await server.transformIndexHtml(url, indexHtml)
}

Use router instance in some composable or any other external file

Hi,

maybe I am missing something, but currently the router instance is only getting returned inside the viteSSR HookParams.

What if I want to use some kind of axios interceptor, in order to automatically redirect the user to some route in case of a 401 for example.

Also I am thinking about implementing an auth composable in which there should be a login and logout method. These need to be able to redirect the user to some routes.

How would you build this kind of thing? Do I really have to build everything router related inside the hook function?

Thank your very much in advance for all your work and effort.

SPA mode: deserialization error

The documentation says one can use SPA mode:

SPA mode: vite dev command runs Vite directly without any SSR.

However this results in deserialization error:

state.js:27 [SSR] On state deserialization - SyntaxError: Unexpected token u in JSON at position 0
    at JSON.parse (<anonymous>)
    at deserializeState (state.js:24)
    at viteSSR2 (entry-client.js:20)
    at main.ts:6 undefined

SSR breaks when adding attributes to the app wrapper div

If you just start a fresh vite project and follow the vite-ssr vue documentation. When you run vite-ssr dev or build an ssr version and host it, you will get an empty page source (just an empty #app <div/>)

Tried debugging it but I cannot find the cause. Is this something you can reproduce?

Don't store full filenames in ssr-manifest.json and main.js

Currently, ssr-manifest.json stores full local filenames:

{
  "/Users/semenov/work/xxx/node_modules/@vue/shared/dist/shared.esm-bundler.js": [
    "/assets/vendor.8b10200d.js"
  ],
  "/Users/semenov/work/xxx/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js": [
    "/assets/vendor.8b10200d.js"
  ],

main.js also has this:

./server/main.js:  (ssrContext.modules || (ssrContext.modules = new Set())).add("/Users/semenov/work/xxx/src/pages/index.vue");
./server/main.js:  (ssrContext.modules || (ssrContext.modules = new Set())).add("/Users/semenov/work/xxx/src/App.vue");
  • That is not safe from the privacy point of view.
  • That most probably means that the package is not redistributable (I am yet to try, but I wouldn't be surprised if it won't work)

Can I use `async setup` with Suspense instead of `serverPrefetch`?

Hi, thanks for all the efforts to create this project !!!

I noticed that Support Suspense in SSR was possible in vue3.
Does this mean I can use fetch data in async setup instead of serverPrefetch ?
I didn't see any example code in the readme shows how to use this plugins with the vue3's setup method, so if I'm wrong would you show me an example that works with the setup method?

Thanks a lot!

Example not work

Hi, i download this project with example and i try start this. After start he say me "Error: Cannot find module 'vite-ssr/build'"

nodejs 15.4

Custom Context Types

Hi, I'm trying to add custom types to context but none of things I tried worked. Is there a way for this?

import {Request, Response} from 'express';

declare module 'vite-ssr' {
   export interface Context {
      request: Request,
      response: Response
   }
}

or

import {Request, Response} from 'express';

declare module 'vite-ssr/vue' {
   export interface Context {
      request: Request,
      response: Response
   }
}

Server-side redirect

vite-ssr currently lacks any reasonable option to perform a server-side redirect.

Even though response object is passed in context, one can't use it like:

export default viteSSR(App, { routes }, ({ response }) => {
  // Simplified version
  // In real code, that could be injected/provided as a consumable hook for Vue pages
  response.setHeader("location", "/foo")
  response.statusCode = 302
  response.end()
})

because vite-ssr will always try to end it itself:

vite-ssr/src/dev/server.ts

Lines 103 to 107 in d758af9

const htmlParts = await render(url, { request, response, ...context })
const html = buildHtmlDocument(template, htmlParts)
response.setHeader('Content-Type', 'text/html')
response.end(html)

Suggestions:

  • at the very least, wrap the 3 last 3 lines like:
  if (!response.writableEnded) {
     const html = buildHtmlDocument(template, htmlParts)
     response.setHeader('Content-Type', 'text/html')
     response.end(html)
  }
  • or rather, allow to inject status, statusText, headers and body not only from options.getRenderContext but also from viteSSR.

Change routes, based on locale detected in base method (way to localize paths)

Hi,

Not sure it's the right place but I'm taking a chance here since it may generate ideas for ways to implement this (in a smart way that I'm tring to) in the future.

I'm trying to give to ViteSSR different routes based on localization.

First idea was to try to set the routes (with router.addroute()) inside of the base method inside ViteSSR export, but it's too late to edit the routes, it absolutely has to happen before.

Then I tried editing the routes in the async(ctx) => {}, and it's working but for the SSR part only, because on hydration, the page disappears. I assume it's because SSR and Vue hydration don't have the same routes to work with.

I'm still learning Vue and I'm sorry if some things seems obvious. I'm using the boilerplate by the author of this package to help me getting started: https://github.com/frandiox/vitesse-ssr-template

Here is what I got to:

export default viteSSR(
   App,
   {
     // Use Router's base for i18n routes
     base: ({ url }) => {
       const locale = extractLocaleFromPath(url.pathname)
       return locale === DEFAULT_LOCALE ? '/' : `/${locale}/`
     },
     routes,
   },
   async(ctx) => {
     // install all modules under `modules/`
     Object.values(import.meta.globEager('./modules/*.ts')).map(i =>
       i.install?.(ctx),
     )
 
     const { app, url, router, isClient, initialState, initialRoute } = ctx
     setLocalizedRoutes(router)
...

setLocalizedRoutes() adds the localized routes to the router (and works, because router.getRoutes() gives me the good localized routes juste under).

I tried setting up before ViteSSR export, but router.currentRoute is always set at / and no way to check the language from URL then, and it makes sense it's not set yet.

Would there be a way to get routes based on language to work in that context? I don't mind losing the instant translation on language switching.

Any help would be welcome, thank you so so much.

[Vue Router warn] No match found for [...] '/node_modules/.vite/chunk...'

vite-ssr v0.10.6
vue-router v4.0.10
vite v2.3.8

Sometimes I'm getting following messages in the console when the server (dev & production) is running:

[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-YSFGVYJQ.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-WM2BNGP4.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-AU43JL3D.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-AJTGOAVB.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-YSFGVYJQ.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-WM2BNGP4.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-AU43JL3D.js?v=7feddd3a"
[Vue Router warn]: No match found for location with path "/node_modules/.vite/chunk-AJTGOAVB.js?v=7feddd3a"

Any idea where this is coming from since the router is configured by vite-ssr? I'm using vite-plugin-pages for the routes.

EDIT 1:
Not sure what caused this but it's gone after reinstalling all dependencies.

EDIT 2:
Happens again for me over and over again when starting the development server. πŸ˜•

Exports and export assignments are not permitted in module augmentations

Hi and thanks for what looks to be a very promising library

node_modules/vite-ssr/index.d.ts:21:3 - error TS2666: Exports and export assignments are not permitted in module augmentations.

Got this while linting the project with vue-tsc --noEmit

// This is a generic mix of framework types
import type { SharedContext, SharedOptions } from './utils/types'

declare module 'vite-ssr' {
  const handler: (
    App: any,
    options: SharedOptions & {
      routes: Array<Record<string, any>>
      routerOptions?: Record<string, any>
      styleCollector?: (ctx: any) => any
    },
    hook?: (
      params: SharedContext & {
        app: any
        router: any
        initialRoute: any
      }
    ) => any
  ) => any

  **export** default handler
  export const ClientOnly: any
  export const useContext: () => SharedContext
}

Updated export default handler to

  export default function (
      App: any,
      options: SharedOptions & {
          routes: Array<Record<string, any>>
          routerOptions?: Record<string, any>
          styleCollector?: (ctx: any) => any
      },
      hook?: (
          params: SharedContext & {
              app: any
              router: any
              initialRoute: any
          }
      ) => any
  ): any

And it stopped complaining.
Unfortunately, my skills in JS/TS only allow me to do so much -- I am completely clueless how to fix that so that the built version of the library on NPM will generate the proper .d.ts file

Anyone any ideas?

Unable to resolve path to module 'vite-ssr'.

Hello,

I'm using eslint with:

  extends: ['plugin:react/recommended', 'airbnb'],

unfortunately when I try to import 'vite-ssr' I have this error:

ESLint: Unable to resolve path to module 'vite-ssr'.(import/no-unresolved)

If someone has an idea :)

Multiple entry points

I'd like to be able to build a separate mobile and desktop app with some shared code, and the same npm dependencies, but different builds. I would expect that most people would want to set up projects in this way, but always find it difficult to do for some reason.

From what I can see, it looks like vite-ssr could make this easier by:

  1. Either allowing configurable entrypoint for both client and server (mobile.html/desktop.html vs index.html), or allowing for dynamically resolving the App that is passed to viteSSR()
  2. Allowing for configurable outDir

At this stage I'm not really sure how feasible this would be, so i might just rewrite or move a file in my package.json scripts until things become easier.

how to use nested route in react-router

It only resolve root of the path pages, but can't resolve some page in child folder.

.pages
β”œβ”€β”€ About.jsx
β”œβ”€β”€ Home.jsx
β”œβ”€β”€ note           
β”‚   β”œβ”€β”€ api.jsx
β”‚   └── index.jsx        // can't resolve
└── notes
    └── :id.jsx

[SSR] On state serialization - TypeError: Converting circular structure to JSON

Hey comunity!

I found myself in weird position where I found something that may be a bug(?) - namely: When this plugin tries to stringify state (in utils/state.js) that is then injected to FE, the JSON.stringify throws and error that theres circular structure.

It throws error like this:

`[SSR] On state serialization - TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'something' -> object with constructor 'Object'
    |     property 'something' -> object with constructor 'Object'
    |     ...
    |     property 'something' -> object with constructor 'Array'
    --- index 0 closes the circle
    at JSON.stringify ()
    at serializeState (/node_modules/vite-ssr/utils/state.js:32:36)
    at eval (/node_modules/vite-ssr/vue/entry-server.js:64:36)
    at runMicrotasks ()
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async handleSsrRequest (C:\Users\Workgroup-L\Desktop\Work\radixal\mortage\mortgage\FE\node_modules\vite-ssr\dev\server.js:88:31)`

Should it use flatted package or have something like this?

Open to make a PR myself! Thanks.

`dev/server.js` may be replaced by vite plugin hooks?

Looking at the dev/server.js, I noticed that the motivation to use such a dedicated file is because these two things

middlewareMode: true,

app.use(async (request, response, next) => {

I think you can utilize these two hooks to get the same result:
https://vitejs.dev/guide/api-plugin.html#config
https://vitejs.dev/guide/api-plugin.html#configureserver

Have I missed anything else? let me know if I'm wrong.

import { createSSRApp } from 'vue' *(Cannot use import statement outside a module)

After I run vite-ssr build, then I follow the example for deploying to Serverless function (Vercel) but i ran into such problem.

There's no error during build phases.

Below are the error while previewing Vercel locally.

C:\Coding\my-vitesse-app\serverless\node_modules\vite-ssr\vue\entry-server.js:1
import { createSSRApp } from 'vue';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:979:16)
    at Module._compile (internal/modules/cjs/loader.js:1027:27)
    at Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Object.require.extensions.<computed> [as .js] (C:\Users\sians\AppData\Roaming\npm\node_modules\vercel\node_modules\ts-node\src\index.ts:832:44)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Module.<anonymous> (C:\Coding\my-vitesse-app\serverless\api\renderer\main.js:21:15)     
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
Error! `node api/index.js` failed with exit code 1

Then I tried to deploy straight to vercel, similar error as such

undefined	ERROR	/var/task/node_modules/vite-ssr/vue/entry-server.js:1
import { createSSRApp } from 'vue';
^^^^^^
SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:979:16)
    at Module._compile (internal/modules/cjs/loader.js:1027:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Module.<anonymous> (/var/task/api/renderer/main.js:9:15)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
RequestId: 0cf21fd7-254a-4550-97c3-c6bc3948db7d Error: Runtime exited with error: exit status 1
Runtime.ExitError

Finally, I've tried deploy as ExpressJS, similar issue happens.

Internal server error: Unexpected token '.'

Hi @frandiox, thanks for such an awesome library.

I'm currently getting the following error when I try to declare and use Vuex store

[vite] Error when evaluating SSR module /src/store/store.getters.ts:
SyntaxError: Unexpected token '.'
    at new Function (<anonymous>)
    at instantiateModule (/Users/xxx/.../node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-6b5f3ba8.js:68222:9)
SyntaxError: Unexpected token '.'
    at new Function (<anonymous>)
    at instantiateModule (/Users/xxx/.../node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-6b5f3ba8.js:68222:9)
3:38:59 PM [vite] Internal server error: Unexpected token '.'

store.getters.ts

import { GetterTree } from 'vuex'
import { StoreState } from './store.state'

const getters: GetterTree<StoreState, StoreState> = {
  isAuthenticated(state) {
    return !!state.user?.uid
  }
}

export default getters

If I removed the optional chaining state.user?.uid and did state.user && state.user.uid it works alright. This has caused me to dynamically import createStore and initialize the store when not in SSR mode

const unwrapStore = () => import('./store/store.index')

export default viteSSR(
  App,
  { routes, base: () => '/' },
  async ({ app, router, initialState, initialRoute }) => {
  
   if (!import.meta.env.SSR) {
      const r = await unwrapStore()
 	  const store = r.default()
      app.use(store, r.key)
      store.replaceState(initialState)
    }
})

Now the problem is that I can't use the store in setup() in SSR mode. So the following will either fail or throw an error cannot reference commit of undefined

setup() {
  const store = useStore()

  store.commit("ACTION", payload)
}

Will this project be written in TypeScript?

Hi, I've just dug around the source code of this project for a while, after comparing to other Vite SSR projects, I believe this is the best SSR plugin for Vite I could find in the market despite the number of stars this project got... I really appreciate your efforts for this project!
But I feel like "unsafe" to modify any options without the type hintings, really wish it can be written in ts for benefitting both the users and contributors πŸ˜€ anyway, great works! Thanks!

Hook after renderToString and/or app.mount

Heya, me again.

(vue.js 3.0 app)

I was reading the source code and noticed that our initialization hook is run early on, which is great, however, in my application (similar to https://pinia.esm.dev/ssr/#state-hydration example) I have my custom data stores.
Trouble is, those are filled with data by components at arbitrary levels deep, and I only have the data once the app has been fully rendered, as I rely on onServerPrefetch(async () => {})

That means that the initialState variable passed to the userland hook can't have the final data the server has fetched, as my hook is ran very early on. I need another hook right after this await

renderToString(app, context).then(deferred.resolve).catch(deferred.reject)
const body = await deferred.promise

To make things consistent on the client end, we could be running that hook after app.mount('#app', true)

Other than that, using this library has been a great experience so far!

initialState adds unnecessary attributes. Extraneous non-props attributes warning.

Hey! Thank you for the great SSR solution! I have an issue with the initialState: when I pass something inside, it adds attributes to the component.

image

The issue resolves by adding a wrapper to the page, but it adds unnecessary attributes 😒
Is there any solution to that?

image

P.S I tested your Vue example and got the same warn.
image

Dynamic imports doesn't work in dev:SSR mode

This works:

setup() {
   const component = defineAsyncComponent(() => import(`../HelloWorld.vue`));
   return { component }; 
}

This doesn't

setup() {
    const name = "HelloWorld";
    const component = defineAsyncComponent(() => import(`../${name}.vue`));
    return { component };
}

Error:

Error: failed to load module for ssr: ../HelloWorld.vue
    at instantiateModule (C:\Projects\vite-ssr\node_modules\vite\dist\node\chunks\dep-2c03f3f9.js:69022:15)

It works when u first load another page then navigate over VueRouter / Dev:SPA also build seems to work.

Don't remove `client/index.html` during build

vite-ssr build removes index.html from the client bundle:

vite-ssr/src/build/index.ts

Lines 113 to 119 in f376c92

// index.html file is not used in SSR and might be
// served by mistake, so let's remove it to avoid issues.
await fs
.unlink(
path.join(clientBuildOptions.build?.outDir as string, 'index.html')
)
.catch(() => null)

I was not expecting this behaviour as a normal Vite SSR build would include the index.html in the client bundle.

For my purposes, I do want the index.html in my client bundle, since I am only selectively using SSR on a few routes on my site, and serving a regular SPA for the rest.

Would it be possible to remove this behaviour from vite-ssr? If the intention is to provide a easy to use SSR plugin for Vite users, I feel that it would be better if vite-ssr's behaviour does not deviate too much from Vite's own SSR support. I would also be happy with a vite-ssr CLI flag to retain index.html!

Happy to submit a PR for either option. Thanks again for this easy-to-use package!

Hard crash (process exit), something causes importing client entry during SSR

This is a spin-off of #43 where I incorrectly identified the problem at first.

Problem

In a small reproducible case, something causes importing client entry during SSR, which leads to hard crash (process exit). I had this on a larger project, but then simplified the setup to bare minimum for the reproduction.

Reproduction

https://github.com/IlyaSemenov/vite-ssr-crash

~/tmp/4
❯ git clone https://github.com/IlyaSemenov/vite-ssr-crash.git
Cloning into 'vite-ssr-crash'...
remote: Enumerating objects: 26, done.
remote: Counting objects: 100% (26/26), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 26 (delta 5), reused 25 (delta 4), pack-reused 0
Receiving objects: 100% (26/26), 13.52 KiB | 157.00 KiB/s, done.
Resolving deltas: 100% (5/5), done.

~/tmp/4
❯ cd vite-ssr-crash

~/tmp/4/vite-ssr-crash master
❯ nodenv local 16.0.0

~/tmp/4/vite-ssr-crash master
❯ yarn
yarn install v1.22.10
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
warning "vite-ssr > @rollup/[email protected]" has unmet peer dependency "rollup@^1.20.0 || ^2.0.0".
warning "vite-ssr > [email protected]" has unmet peer dependency "react@^16.8.0 || ^17.0.0".
warning "vite-ssr > @rollup/plugin-replace > @rollup/[email protected]" has unmet peer dependency "rollup@^1.20.0||^2.0.0".
warning Workspaces can only be enabled in private projects.
[4/4] πŸ”¨  Building fresh packages...
✨  Done in 1.69s.

~/tmp/4/vite-ssr-crash master
❯ yarn dev
yarn run v1.22.10
$ vite-ssr
Pre-bundling dependencies:
  vite-ssr
  vue
  vite-ssr/vue/entry-client
  vite-ssr/vue/entry-server
(this will be run only when your dependencies or config have changed)

  vite v2.3.8 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

 -- SSR mode

>>>>>>>>>>>>>> (in a separate console) curl localhost:3000

[Function (anonymous)]
TypeError: render is not a function
    at handleSsrRequest (/Users/semenov/tmp/4/vite-ssr-crash/node_modules/vite-ssr/dev/server.js:88:37)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
12:57:31 AM [vite] Internal server error: render is not a function
      at handleSsrRequest (/Users/semenov/tmp/4/vite-ssr-crash/node_modules/vite-ssr/dev/server.js:88:37)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
/node_modules/vite-ssr/vue/entry-client.js:20
    const url = window.location;
                ^

ReferenceError: window is not defined
    at Module.viteSSR [as default] (/node_modules/vite-ssr/vue/entry-client.js:20:17)
    at eval (/Users/semenov/tmp/4/vite-ssr-crash/src/main.ts:12:61)
    at instantiateModule (/Users/semenov/tmp/4/vite-ssr-crash/node_modules/vite/dist/node/chunks/dep-0ed4fbc0.js:69982:166)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

The two key points are:

  • non-empty alias section in vite config
  • importing (not even using - just importing and accessing) onServerPrefetch

UPDATE:

I came up with even simpler version without onServerPrefetch, which crashes the same way.

https://github.com/IlyaSemenov/vite-ssr-crash/tree/no_routes

git clone https://github.com/IlyaSemenov/vite-ssr-crash.git -b no_routes

Any way to make a server local variable for every request?

Hello πŸ‘‹ I want to create a variable, that is from the initialState, but is local for only one request.

In this example, I have a stack, that adds and pops numbers. The value is coming from the initialState. Everything works fine, except, that the initialState stays the same for every page refresh. Is there a way to define a "local" kinda variable?

import { useContext } from 'vite-ssr'
import { ref, readonly } from 'vue'

const state = ref<number[]>([])

export function useStack() {
  const context = useContext()

  if (context.isClient) {
    state.value = context.initialState.stack || []
  }

  return {
    add(n: number) {
      state.value.push(n)
      context.initialState.stack = state.value
    },

    pop() {
      state.value.pop()
      context.initialState.stack = state.value
    },

    get state() {
      return readonly(state.value)
    },
  }
}
<script lang="ts" setup>
import { onMounted, onServerPrefetch } from 'vue'
import { useStack } from './composables'

const stack = useStack()

onMounted(() => {
  console.log(stack.state) // expect to be [1] on every request, but get [1, ...1 * n request]
})

onServerPrefetch(() => {
  stack.add(1)
})
</script>

<template>
  <pre>{{ stack.state }}</pre>
</template>

Route params not exposed as props in SPA mode

Say I define my routes as follows:

export const routes = [
  {
    path: '/',
    component: () => import('./pages/Home.vue'),
    name: Routes.Home,
  },
  {
    name: 'UserProfile',
    path: '/user/:uid',
    component: () => import('./pages/User.vue'),
    props: true,
  },
  {
    path: '/:catchAll(.*)',
    name: 'not-found',
    component: () => import('./pages/404.vue'),
  }
]

...In SPA mode, I don't get the vid param as a prop and in SSR mode, I have to patch to.meta.state as shown below to get vid param injected into the component as a prop.

router.beforeEach((to, from, next) => {
   if (to.meta.state) {
       return next()
   }
   // pass route params to the page as props
   to.meta.state = Object.assign(
    {},
    to.meta.state,
    initialState,
    initialRoute.params,
   )
  next()
})

I guess I'm not doing something right

Template refs not available in ClientOnly

When a template ref is applied to a VNode slotted in ClientOnly, the ref is never available

Example

<!-- This template won't work -->
<template lang="pug">
.relative: ClientOnly: .absolute.inset-0(ref="foo")
</template>

<!-- This template works -->
<template lang="pug">
.relative: .absolute.inset-0(ref="foo")
</template>

<!-- Log ref in watcher & mount hook -->
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'

const foo = ref<HTMLDivElement | null>(null)

watch(foo, x => console.info(x))
onMounted(() => console.info(foo?.value))
</script>

package.json for afffected project

{
  "private": true,
  "engines": {
    "node": ">=14"
  },
  "scripts": {
    "bi": "bi --pm pnpm",
    "fetch": "node ./scripts/fetch-metadata",
    "gen": "node ./scripts/generate-entities",
    "dev": "vite-ssr --port 3333 --open",
    "dev:spa": "vite --port 3333 --open",
    "clean": "rm -rf dist; rm -rf api/renderer",
    "build": "pnpm clean; cross-env NODE_ENV=production vite-ssr build; ./scripts/move-dist.sh",
    "preview": "vc dev"
  },
  "dependencies": {
    "@replygirl/tc": "^3.0.0",
    "@vue/reactivity": "^3.0.11",
    "@vue/server-renderer": "^3.0.11",
    "@vueuse/core": "^4.9.3",
    "@vueuse/head": "^0.5.1",
    "axios": "^0.21.1",
    "breeze-client": "^2.1.1",
    "change-case": "^4.1.2",
    "element-plus": "^1.0.2-beta.44",
    "graphql": "^15.5.0",
    "hash-sum": "^2.0.0",
    "node-fetch": "^2.6.1",
    "nprogress": "^0.2.0",
    "pluralize": "^8.0.0",
    "prism-theme-vars": "^0.2.2",
    "querystring": "^0.2.1",
    "vite-ssr": "^0.8.1",
    "vue": "^3.0.11",
    "vue-i18n": "^9.1.6",
    "vue-router": "^4.0.6",
    "vue3-google-map": "^0.7.6"
  },
  "devDependencies": {
    "@antfu/eslint-config": "^0.6.5",
    "@iconify/json": "^1.1.341",
    "@intlify/vite-plugin-vue-i18n": "^2.1.2",
    "@replygirl/curse": "^3.0.0-beta.0",
    "@types/apple-mapkit-js-browser": "^5.50.0",
    "@types/axios": "^0.14.0",
    "@types/btoa": "^1.2.3",
    "@types/dotenv-flow": "^3.1.0",
    "@types/eslint": "^7.2.10",
    "@types/eslint-config-prettier": "^6.11.0",
    "@types/eslint-plugin-prettier": "^3.1.0",
    "@types/hash-sum": "^1.0.0",
    "@types/node": "^14.14.43",
    "@types/node-fetch": "^2.5.10",
    "@types/nprogress": "^0.2.0",
    "@types/pluralize": "^0.0.29",
    "@types/prettier": "^2.2.3",
    "@types/pug": "^2.0.4",
    "@types/rimraf": "^3.0.0",
    "@typescript-eslint/eslint-plugin": "^4.23.0",
    "@vercel/node": "^1.10.0",
    "@vitejs/plugin-vue": "^1.2.2",
    "@vue/compiler-sfc": "^3.0.11",
    "better-install": "^0.1.0",
    "breeze-entity-generator": "^1.0.0",
    "btoa": "^1.2.1",
    "cross-env": "^7.0.3",
    "dotenv-flow": "^3.2.0",
    "eslint": "^7.26.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^3.4.0",
    "markdown-it-prism": "^2.1.6",
    "pnpm": "^6.3.0",
    "prettier": "2.3.0",
    "pug": "^3.0.2",
    "rimraf": "^3.0.2",
    "ts-node": "^9.1.1",
    "type-fest": "^1.1.1",
    "typescript": "^4.2.4",
    "vite": "^2.3.0",
    "vite-plugin-components": "^0.9.0",
    "vite-plugin-icons": "^0.5.0",
    "vite-plugin-md": "^0.6.6",
    "vite-plugin-pages": "^0.11.1",
    "vite-plugin-pwa": "^0.7.3",
    "vite-plugin-vue-layouts": "^0.3.1",
    "vite-plugin-windicss": "^0.15.10",
    "vite-ssg": "^0.10.0"
  }
}

Double JSON stringifying

I noticed that the __INITIAL_STATE__ object that was being inserted into the HTML had lots of escaped double quote characters (i.e., \"), and it was because the entire initial state object was itself a string. Apparently the initial state is put through JSON.stringify twice before unsafe characters are escaped! That seemed odd and unnecessary to me, so in my project I changed that part of the code in serializeState to JSON.stringify only once and in deserializeState to simply return state || {} without extra parsing. It turns out this works just fineβ€”all unsafe characters are properly escaped, and there is far less overhead in transmitting/parsing the initial state object this way.

A couple questions:

  • Is there a real reason the initial state object is first serialized to a JSON string and then that JSON string is itself stringified again?
  • If not, would you accept a PR that removes the needless extra JSON.stringify?

bootsrap

how can I load bootrap 5 in client only?

Add initialState example with a store (vuex/pinia)

Hey!

The docs describe the use of Vuex for the initial state but I'm not 100% sure how I need to use that in a component.
For me, it sounds like that I either need to use app.provide (& inject in components) or a store like Vuex. Is this correct?

Currently my code sets the state of the store to the value of the initialStore parameter. The value is available in my components, but only on the client-side (I think?), because it shows the title and instantly deletes it. Do I still need to set the initial state to that value in the component like it's in the Homepage.vue example when I use a store?

This is my setup() code of my component:

const store = useStore()
const title = ref(store.title || null)

if (!title.value) {
  onServerPrefetch(() => {
    title.value = 'Fetched title'
    store.$patch({
      title: title.value
    })
  })
}

The same code works when I inject the initialState and set it like initialState.title = title.value.

EDIT:
For reference there is actually this documentation (https://pinia.esm.dev/ssr/#state-hydration) for pinia in their docs which works perfectly.

Extract Apollo cache for SSR

Hello,

I'm trying to use Apollo Client with SSR and React.
My idea was to create a InMemoryCache and extract data from the store. It seams that viteSSR does not wait the resolution of a query. If I add a setTimeout to the hook of viteSSR, I cant print the good content of the store.
According the Apollo Documentation, I have to use renderToStringWithData. But I have no idea how I can manage with vite-ssr plugin. Is it possible?

Thank you very much.

./server/index.js

// This is the server renderer we just built
const main = import('../dist/server/main.js');

server.get('*', async (req, res) => {
  const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
  const renderPage = (await main).default.default;

  const apolloCache = new InMemoryCache();

  const { html } = await renderPage(url, {
    manifest,
    apolloCache,
    preload: true,
  });

  res.setHeader('Cache-Control', 'max-age=0');
  res.end(html);
});

./src/App.jsx

const App = ({
  isClient,
  apolloCache = new InMemoryCache(),
}) => {
  const client = new ApolloClient({
    link: createHttpLink({
      uri: 'http://localhost:8080/graph',
      credentials: 'same-origin',
    }),
    ssrMode: !isClient,
    cache: apolloCache,
    credentials: 'same-origin',
  });
...

./src/main.jsx

export default viteSSR(App, { routes }, ({
  initialState,
  apolloCache,
}) => {
  if (import.meta.env.SSR) {
    setTimeout(() => {
      console.log(apolloCache.extract()) // not empty
    }, 1000)
    initialState.apolloCache = apolloCache.extract();
  }
  // Custom initialization hook
})

Way to pass router configuration [VUE]

Hi - great package!

What do you think about adding the possibility to pass router configuration to the viteSSR? Right now it's not possible to leverage other configuration options like scrollBehavior or linkActiveClass.

Happy to work sth out and submit a PR as well.

Outdir option not working

When you have outDir option in vite.config.ts vite-ssr doesn't follow the rule and bundles every file to dist in main folder.

Base path

Hi,
is there possibility to use this configuration https://vitejs.dev/config/#base to configure base path?

In my case it is not working if I use this in vite.config.ts with this configuration: "build": "node node_modules/vite-ssr/cli.js build",

HMR Infinite Loop with wss settings

Our vite config has the following config to connect via ssl in dev.

  server: {
    hmr: {
      host: 'leveluptutorials.dev',
      port: 443,
      protocol: 'wss',
    },
  },

This config works fine with the standard vite cli, but causes an infinite loop using vite ssr. It's hard to debug because of the nature of things, but it seems like the wss connection is failing causing a refresh on loop.

The network shows a 502 request.

Request URL: https://leveluptutorials.dev/__vite_ping
Request Method: GET
Status Code: 502 
Remote Address: 127.0.0.1:443
Referrer Policy: strict-origin-when-cross-origin

It's kind of tough to put together a reproduction given the ssl requirements, I can provide as much as possible to help and can dive into your code to see if there is anything that sticks out.

Edit:
Some additional information. The looping we're seeing is the same as if the hmr config settings were not set at all. This tells me that this library might not be seeing the server.hmr options.

Crash on Vue 3.1 using onServerPrefetch

vite-ssr crashes when using onServerPrefetch.

Example code:

<script lang="ts">
import { defineComponent, onServerPrefetch, ref } from "vue"

export default defineComponent({
	setup() {
		const data = ref("initial")
		onServerPrefetch(async () => {
			data.value = "server"
		})
		return { data }
	},
})
</script>

<template>
	<div>data = {{ data }}</div>
</template>

Steps to reproduce

~/tmp/2
❯ git clone https://github.com/IlyaSemenov/vite-ssr-crash.git
Cloning into 'vite-ssr-crash'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 13 (delta 0), reused 13 (delta 0), pack-reused 0
Receiving objects: 100% (13/13), 12.28 KiB | 172.00 KiB/s, done.

~/tmp/2
❯ cd vite-ssr-crash

~/tmp/2/vite-ssr-crash master
❯ nodenv local 16.0.0

~/tmp/2/vite-ssr-crash master
❯ yarn
yarn install v1.22.10
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
warning "vite-ssr > @rollup/[email protected]" has unmet peer dependency "rollup@^1.20.0 || ^2.0.0".
warning "vite-ssr > [email protected]" has unmet peer dependency "react@^16.8.0 || ^17.0.0".
warning "vite-ssr > @rollup/plugin-replace > @rollup/[email protected]" has unmet peer dependency "rollup@^1.20.0||^2.0.0".
warning Workspaces can only be enabled in private projects.
[4/4] πŸ”¨  Building fresh packages...
✨  Done in 1.65s.

~/tmp/2/vite-ssr-crash master
❯ yarn dev
yarn run v1.22.10
$ vite-ssr
Pre-bundling dependencies:
  vite-ssr
  @vue/runtime-core
  vite-ssr/vue/entry-client
  vite-ssr/vue/entry-server
(this will be run only when your dependencies or config have changed)

  vite v2.3.8 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

 -- SSR mode

TypeError: render is not a function
    at handleSsrRequest (/Users/semenov/tmp/2/vite-ssr-crash/node_modules/vite-ssr/dev/server.js:88:37)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
10:40:51 PM [vite] Internal server error: render is not a function
      at handleSsrRequest (/Users/semenov/tmp/2/vite-ssr-crash/node_modules/vite-ssr/dev/server.js:88:37)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
/node_modules/vite-ssr/vue/entry-client.js:20
    const url = window.location;
                ^

ReferenceError: window is not defined
    at Module.viteSSR [as default] (/node_modules/vite-ssr/vue/entry-client.js:20:17)
    at eval (/Users/semenov/tmp/2/vite-ssr-crash/src/main.ts:12:61)
    at instantiateModule (/Users/semenov/tmp/2/vite-ssr-crash/node_modules/vite/dist/node/chunks/dep-0ed4fbc0.js:69982:166)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Add built-in production server and expose server APIs

It doesn't make sense to re-implement production server every time.

Similarly to vite preview, there could be vite-ssr preview that runs a local web server.

Also, there should be lower-level exports (say in vite-ssr/server) for:

  • renderHtml (so that the end user doesn't need to deal with importing from dist folder, suppress type warnings, etc.)
  • createApp or something along the lines, an Express/Connect app that doesn't listen by itself but could be injected into own http server

Build error with plugin legacy

If you use @vitejs/plugin-legacy in the project and try to build server, this error occures

A:\Desktop\VUE\kyzc-portfolio\node_modules\.pnpm\[email protected]_1875ae38a9ea4201aa9b0ba5cafe20a3\node_modules\vite-ssr\build\index.js:20
    const indexHtml = clientResult.output.find((file) => file.type === 'asset' && file.fileName === 'index.html');
                                          ^

TypeError: Cannot read property 'find' of undefined
    at module.exports (A:\Desktop\VUE\kyzc-portfolio\node_modules\.pnpm\[email protected]_1875ae38a9ea4201aa9b0ba5cafe20a3\node_modules\vite-ssr\build\index.js:20:43)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async A:\Desktop\VUE\kyzc-portfolio\node_modules\.pnpm\[email protected]_1875ae38a9ea4201aa9b0ba5cafe20a3\node_modules\vite-ssr\cli.js:19:9
 ERROR  Command failed with exit code 1.

Module Node Server Export

We need a module export option for node applications that runs with mjs.

For example I have a fastify server with module system and I cannot import dist/server/main.js becauase it uses require.

There can be an option like this for module nodejs or vite-ssr can bundle another file as main.mjs

plugins: [
   ssr({
     build: {
        esm: true
     }
   })
]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.