]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat: support raw Vue SSR (#90)
authorJohannes Lamberts <mail@j-lamberts.de>
Mon, 6 Apr 2020 09:18:07 +0000 (11:18 +0200)
committerGitHub <noreply@github.com>
Mon, 6 Apr 2020 09:18:07 +0000 (11:18 +0200)
* feat: support raw Vue SSR

* test: warn clients on ssrPlugin install

* refactor: remove unnecessary branch

* test: ignore coverage on path not reachable without modifying serverPrefetch option merge

* refactor: simulate node-environment with jest

* refactor: adjust warn message for wrong environment

README.md
__tests__/ssr/app.spec.ts
__tests__/ssr/app/App.ts
__tests__/ssr/app/entry-server.ts
__tests__/ssr/app/main.ts
__tests__/ssr/ssrPlugin.spec.ts [new file with mode: 0644]
nuxt/plugin.js
src/index.ts
src/ssrPlugin.ts [new file with mode: 0644]

index ca6845eef36b9ad84ed26ce73ff54f86a3814442..a7f0aa0c3a75d533ceca6518719bb0de41618763 100644 (file)
--- a/README.md
+++ b/README.md
@@ -289,7 +289,40 @@ It may look like things are working even if you don't pass `req` to `useStore` *
 
 #### Raw Vue SSR
 
-TODO: this part isn't built yet. You need to call `setActiveReq` with the _Request_ object before `useStore` is called
+In a Raw Vue SSR application you have to modify a few files to enable hydration and to tell requests apart.
+
+```js
+// entry-server.js
+import { getRootState, PiniaSsr } from "pinia";
+
+// install plugin to automatically use correct context in setup and onServerPrefetch
+Vue.use(PiniaSsr);
+
+export default context => {
+  /* ... */
+  context.rendered = () => {
+    // pass state to context
+    context.piniaState = getRootState(context.req);
+  };
+ /* ... */
+};
+```
+
+```html
+<!-- index.html -->
+<body>
+<!-- pass state from context to client -->
+{{{ renderState({ contextKey: 'piniaState', windowKey: '__PINIA_STATE__' }) }}}
+</body>
+```
+
+```js
+// entry-client.js
+import { setStateProvider } from "pinia";
+
+// inject ssr-state
+setStateProvider(() => window.__PINIA_STATE__);
+```
 
 ### Accessing other Stores
 
index 8c78f9b2e41c13a363588583ca185d908ff9e8e9..147bce3b9693d1b1e7f5073043878b8c15a097d0 100644 (file)
@@ -1,39 +1,47 @@
+/**
+ * @jest-environment node
+ */
+
 import renderApp from './app/entry-server'
 import { createRenderer } from 'vue-server-renderer'
 
 const renderer = createRenderer()
 
+function createContext() {
+  return {
+    rendered: () => {},
+    req: {},
+  }
+}
+
 describe('classic vue app', () => {
   it('renders using the store', async () => {
-    const context = {
-      rendered: () => {},
-    }
+    const context = createContext()
     const app = await renderApp(context)
 
     // @ts-ignore
-    const html = await renderer.renderToString(app)
+    const html = await renderer.renderToString(app, context)
     expect(html).toMatchInlineSnapshot(
       `"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
     )
   })
 
   it('resets the store', async () => {
-    const context = {
-      rendered: () => {},
-    }
+    let context = createContext()
     let app = await renderApp(context)
 
     // @ts-ignore
-    let html = await renderer.renderToString(app)
+    let html = await renderer.renderToString(app, context)
     expect(html).toMatchInlineSnapshot(
       `"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
     )
 
-    // render again
+    // render again with new request context
+    context = createContext()
     app = await renderApp(context)
 
     // @ts-ignore
-    html = await renderer.renderToString(app)
+    html = await renderer.renderToString(app, context)
     expect(html).toMatchInlineSnapshot(
       `"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
     )
index 810dbb3c1db77c705da59a004ba67f172727820f..5699a4d2ef30b0217fc8d5f17eb6c93182e23004 100644 (file)
@@ -2,6 +2,11 @@ import { defineComponent, computed } from '@vue/composition-api'
 import { useStore } from './store'
 
 export default defineComponent({
+  async serverPrefetch() {
+    const store = useStore()
+    store.state.counter++
+  },
+
   setup() {
     const store = useStore()
 
index 71190c179c26dcd5d7bb075ca69534bc669323cb..e6bac60f125541cc264f452967460b570522ffad 100644 (file)
@@ -1,8 +1,12 @@
+import Vue from 'vue'
 import { createApp } from './main'
+import { PiniaSsr, getRootState } from '../../../src'
+
+Vue.use(PiniaSsr)
 
 export default function(context: any) {
   return new Promise(resolve => {
-    const { app, store } = createApp()
+    const { app } = createApp()
 
     // This `rendered` hook is called when the app has finished rendering
     context.rendered = () => {
@@ -11,7 +15,7 @@ export default function(context: any) {
       // When we attach the state to the context, and the `template` option
       // is used for the renderer, the state will automatically be
       // serialized and injected into the HTML as `window.__INITIAL_STATE__`.
-      context.state = store.state
+      context.state = getRootState(context.req)
     }
 
     resolve(app)
index ea8fa3621610aae918dc4dfac5d28641e440f4cf..88fcd2aa1a8ece19ff87f9a691a94b56da25bd89 100644 (file)
@@ -1,24 +1,16 @@
 import Vue from 'vue'
 // import VueCompositionApi from '@vue/composition-api'
 import App from './App'
-import { useStore } from './store'
-import { setActiveReq } from '../../../src'
 
 // Done in setup.ts
 // Vue.use(VueCompositionApi)
 
 export function createApp() {
-  // create router and store instances
-  setActiveReq({})
-  const store = useStore()
-
-  store.state.counter++
-
   // create the app instance, injecting both the router and the store
   const app = new Vue({
     render: h => h(App),
   })
 
   // expose the app, the router and the store.
-  return { app, store }
+  return { app }
 }
diff --git a/__tests__/ssr/ssrPlugin.spec.ts b/__tests__/ssr/ssrPlugin.spec.ts
new file mode 100644 (file)
index 0000000..f67b8a8
--- /dev/null
@@ -0,0 +1,12 @@
+import Vue from 'vue'
+import { PiniaSsr } from '../../src'
+
+it('should warn when installed in the browser', () => {
+  const mixinSpy = jest.spyOn(Vue, 'mixin')
+  const warnSpy = jest.spyOn(console, 'warn')
+  Vue.use(PiniaSsr)
+  expect(warnSpy).toHaveBeenCalledWith(
+    expect.stringMatching(/seems to be used in the browser bundle/i)
+  )
+  expect(mixinSpy).not.toHaveBeenCalled()
+})
index cde0d6d4a3da1ff06ff8b39d7679cb488b83ad31..4300e7b29b8d41f87b503c7c3b36525a9413b1d7 100644 (file)
@@ -1,45 +1,11 @@
 // @ts-check
 import Vue from 'vue'
 // @ts-ignore: this must be pinia to load the local module
-import { setActiveReq, setStateProvider, getRootState } from 'pinia'
+import { setActiveReq, PiniaSsr, setStateProvider, getRootState } from 'pinia'
 
-Vue.mixin({
-  beforeCreate() {
-    // @ts-ignore
-    const { setup, serverPrefetch } = this.$options
-    if (setup) {
-      // @ts-ignore
-      this.$options.setup = (props, context) => {
-        if (context.ssrContext && context.ssrContext.req) {
-          setActiveReq(context.ssrContext.req)
-        }
-
-        return setup(props, context)
-      }
-    }
-
-    if (process.server && serverPrefetch) {
-      const patchedServerPrefetch = Array.isArray(serverPrefetch)
-        ? serverPrefetch.slice()
-        : [serverPrefetch]
-
-      for (let i = 0; i < patchedServerPrefetch.length; i++) {
-        const original = patchedServerPrefetch[i]
-        /**
-         * @type {(this: import('vue').default) => any}
-         */
-        patchedServerPrefetch[i] = function() {
-          setActiveReq(this.$ssrContext.req)
-
-          return original.call(this)
-        }
-      }
-
-      // @ts-ignore
-      this.$options.serverPrefetch = patchedServerPrefetch
-    }
-  },
-})
+if (process.server) {
+  Vue.use(PiniaSsr)
+}
 
 /** @type {import('@nuxt/types').Plugin} */
 const myPlugin = context => {
index 4ca79b37ccef4b34524f292b501232d5ea0e68b5..0c34756552f24a145904efb818ce2688705ec601 100644 (file)
@@ -1,3 +1,4 @@
 export { createStore } from './store'
 export { setActiveReq, setStateProvider, getRootState } from './rootStore'
 export { StateTree, StoreGetter, Store } from './types'
+export { PiniaSsr } from './ssrPlugin'
diff --git a/src/ssrPlugin.ts b/src/ssrPlugin.ts
new file mode 100644 (file)
index 0000000..838b4cf
--- /dev/null
@@ -0,0 +1,48 @@
+import { VueConstructor } from 'vue/types'
+import { setActiveReq } from './rootStore'
+
+export const PiniaSsr = (vue: VueConstructor) => {
+  const isServer = typeof window === 'undefined'
+
+  if (!isServer) {
+    console.warn(
+      '`PiniaSsrPlugin` seems to be used in the browser bundle. You should only call it on the server entry: https://github.com/posva/pinia#raw-vue-ssr'
+    )
+    return
+  }
+
+  vue.mixin({
+    beforeCreate() {
+      const { setup, serverPrefetch } = this.$options
+      if (setup) {
+        this.$options.setup = (props, context) => {
+          // @ts-ignore
+          setActiveReq(context.ssrContext.req)
+          return setup(props, context)
+        }
+      }
+
+      if (serverPrefetch) {
+        const patchedServerPrefetch = Array.isArray(serverPrefetch)
+          ? serverPrefetch.slice()
+          : // serverPrefetch not being an array cannot be triggered due tue options merge
+            // https://github.com/vuejs/vue/blob/7912f75c5eb09e0aef3e4bfd8a3bb78cad7540d7/src/core/util/options.js#L149
+            /* istanbul ignore next */
+            [serverPrefetch]
+
+        for (let i = 0; i < patchedServerPrefetch.length; i++) {
+          const original = patchedServerPrefetch[i]
+          patchedServerPrefetch[i] = function() {
+            // @ts-ignore
+            setActiveReq(this.$ssrContext.req)
+
+            return original.call(this)
+          }
+        }
+
+        // @ts-ignore
+        this.$options.serverPrefetch = patchedServerPrefetch
+      }
+    },
+  })
+}