From: Johannes Lamberts Date: Mon, 6 Apr 2020 09:18:07 +0000 (+0200) Subject: feat: support raw Vue SSR (#90) X-Git-Tag: 0.0.6~44 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=91d7b380868f53a8ed2fc14ca7a5dbb4d81493f5;p=thirdparty%2Fvuejs%2Fpinia.git feat: support raw Vue SSR (#90) * 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 --- diff --git a/README.md b/README.md index ca6845ee..a7f0aa0c 100644 --- 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 + + + +{{{ renderState({ contextKey: 'piniaState', windowKey: '__PINIA_STATE__' }) }}} + +``` + +```js +// entry-client.js +import { setStateProvider } from "pinia"; + +// inject ssr-state +setStateProvider(() => window.__PINIA_STATE__); +``` ### Accessing other Stores diff --git a/__tests__/ssr/app.spec.ts b/__tests__/ssr/app.spec.ts index 8c78f9b2..147bce3b 100644 --- a/__tests__/ssr/app.spec.ts +++ b/__tests__/ssr/app.spec.ts @@ -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( `"

Hi anon

Count: 1 x 2 = 2

"` ) }) 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( `"

Hi anon

Count: 1 x 2 = 2

"` ) - // 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( `"

Hi anon

Count: 1 x 2 = 2

"` ) diff --git a/__tests__/ssr/app/App.ts b/__tests__/ssr/app/App.ts index 810dbb3c..5699a4d2 100644 --- a/__tests__/ssr/app/App.ts +++ b/__tests__/ssr/app/App.ts @@ -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() diff --git a/__tests__/ssr/app/entry-server.ts b/__tests__/ssr/app/entry-server.ts index 71190c17..e6bac60f 100644 --- a/__tests__/ssr/app/entry-server.ts +++ b/__tests__/ssr/app/entry-server.ts @@ -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) diff --git a/__tests__/ssr/app/main.ts b/__tests__/ssr/app/main.ts index ea8fa362..88fcd2aa 100644 --- a/__tests__/ssr/app/main.ts +++ b/__tests__/ssr/app/main.ts @@ -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 index 00000000..f67b8a8a --- /dev/null +++ b/__tests__/ssr/ssrPlugin.spec.ts @@ -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() +}) diff --git a/nuxt/plugin.js b/nuxt/plugin.js index cde0d6d4..4300e7b2 100644 --- a/nuxt/plugin.js +++ b/nuxt/plugin.js @@ -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 => { diff --git a/src/index.ts b/src/index.ts index 4ca79b37..0c347565 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 index 00000000..838b4cff --- /dev/null +++ b/src/ssrPlugin.ts @@ -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 + } + }, + }) +}