freddy38510 / quasar-app-extension-ssg

Static Site Generator App Extension for Quasar.
MIT License
149 stars 17 forks source link

Using Pinia from library results in `TypeError: can't access property "_s", pinia is undefined` #379

Open renyuneyun opened 6 months ago

renyuneyun commented 6 months ago

(This is a further attempt after #375. The previous issue contains useful background, but may not be directly related to this issue.)

I have managed to successfully build a static site from my project. However, I encounter the error related to pinia when opening it in browser.

I have produced a MRE at https://github.com/renyuneyun/minimal-reproduction/tree/5779796a11a816d5e4958baf4f60b893868c28da/quasar-solid-ssg, with instructions in README.

(There is also the circular object issue. Shall I open a separate GH Issue for that?)

freddy38510 commented 6 months ago

You should add the pinia package to your app dependencies.

This lead to the circular object issue which is related to store state hydration. There is some circular references in the Session object.

When pre-rendering at server side, the store state is serialized and inlined in the page markup. Later when the page is rendered at client-side, the store is primed with server-initialized state.

The issue happens when serializing the state.

Anyway, I assume you don't want to initialize the client-side store state with a Session instantiated during SSR, so you can proceed as follows:

// https://github.com/renyuneyun/solid-helper-vue/blob/master/src/stores/session.ts

/**
 * The Session object. Reactivity is lost. Useful mainly for its functions (e.g. `fetch()`)
 */
session: typeof window !== 'undefined' ? new Session() : undefined, // no Session at server-side

Note that you can also serialize the state manually.

First enable the manualStoreSerialization option in the quasar.config.js file:

ssr: {
  manualStoreSerialization: true
},

Then create a Quasar boot file:

// src/boot/serialize-state.ts

import { boot } from 'quasar/wrappers'
import { stringify } from 'flatted'; // this package is used to serialize state, it handles circular object,
// You will need to add it to  your app dependencies

// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async ({ ssrContext }) => {
  if(!ssrContext) {
    return;
  }

  ssrContext.onRendered(() => {
    // at that time the store state is available in ssrContext.state
    // the lines below add a <script> to the pre-rendered markup that injects the serialized state into window.__INITIAL_STATE__
    // window.__INITIAL_STATE__ is used at client-side to prime the store

    const autoRemove = 'document.currentScript.remove()'

    ssrContext._meta.headTags = `<script>window.__INITIAL_STATE__=${stringify(ssrContext.state)};${autoRemove}</script>`+ ssrContext._meta.headTags;
  })
})

And only use it server-side:

// quasar.config.js

boot: [{
  client: false,
  path: 'serialize-state'
}],
renyuneyun commented 6 months ago

Thanks for the explanation a lot. However, after applying the first patch, another (or a related?) Pinia issue is still there:

TypeError: pinia is undefined
    useStore pinia.mjs:1705
    setup IndexPage.vue:15
    callWithErrorHandling runtime-core.esm-bundler.js:158
    setupStatefulComponent runtime-core.esm-bundler.js:7331
    setupComponent runtime-core.esm-bundler.js:7292
    mountComponent runtime-core.esm-bundler.js:5687
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateChildren runtime-core.esm-bundler.js:4853
    hydrateElement runtime-core.esm-bundler.js:4809
    hydrateNode runtime-core.esm-bundler.js:4672
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateChildren runtime-core.esm-bundler.js:4853
    hydrateElement runtime-core.esm-bundler.js:4809
    hydrateNode runtime-core.esm-bundler.js:4672
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrateSubTree runtime-core.esm-bundler.js:5763
    componentUpdateFn runtime-core.esm-bundler.js:5783
    run reactivity.esm-bundler.js:178
    update runtime-core.esm-bundler.js:5902
    setupRenderEffect runtime-core.esm-bundler.js:5910
    mountComponent runtime-core.esm-bundler.js:5700
    hydrateNode runtime-core.esm-bundler.js:4691
    hydrate runtime-core.esm-bundler.js:4555
    mount runtime-core.esm-bundler.js:3853
    mount runtime-dom.esm-bundler.js:1486
    start client-entry.js:72
    promise callback*start client-entry.js:70
    promise callback* client-entry.js:79

[runtime-core.esm-bundler.js:226:12](http://127.0.0.1:4000/@vue/runtime-core/dist/runtime-core.esm-bundler.js)
Hydration completed but contains mismatches.

This can be reproduced regardless of which variant used in the other repo. After clicking the line of code (i.e. the link with useStore pinia.mjs:1705), it jumps to a pinia source file under the helper library (solid-helper-vue/node_modules/pinia/), rather than a pinia source file under the main/application project. I can confirm that Pinia is installed in the main/application project, and a separate pinia source file exists in node_modules/pinia/. (I also tried to remove the node_modules directory in my filesystem under solid-helper-vue, which seems to be irrelevant.)

Presumably this is caused because that separate pinia source results in a separate "instance" of pinia which is not properly initialized? It seems somehow the dependency resolution / reproduction in the SSR/SSG is problematic? Or maybe I misconfigured somewhere, which made pinia not fully externalized or not recognized?

freddy38510 commented 6 months ago

The vue and pinia packages should not be direct dependencies of your library. You should add them as peerDependencies. It is the responsibility of the consumer of your library to install them. In your case, the consumer is your Quasar application.

For the @inrupt/solid-client-authn-browser package, you can add it as a direct dependency of your library, because your library seems to using it internally to provide authentication via actions and exposes the Session via its store state. It means you should also remove this package from your Quasar app.

You should also add vue and pinia to the rollup external option. The @inrupt/solid-client-authn-browser package should be added to the rollup external option, only for ESM output format, because it will be handled by the consumer via a bundler (vite, webpack, etc...). The only case this package should be bundled in your library (not added to the rollup external option) is for the UMD output format.

Your issue about pinia occurs because the dep is loaded from your library instead of your Quasar app. This is because the dep is resolved locally ../../solid-helpers-vue where Node.js find a node_modules folder containing the pinia package. To solve this, I would recommend you to use yalc for development purposes.

To use your library correctly for SSR, don't forget to change this line:

session: typeof window !== 'undefined' ? new Session() : null

It should be set to null and not undefined for SSR, otherwise, the session property will not be defined when the store state will be primed with the server-initialized state at client-side, therefore it cannot be modified later.

To actually create a new Session instance at client-side, I would recommend you these lines of code:

import { onMounted } from 'vue';
import { useSessionStore } from 'solid-helper-vue';

const sessionStore = useSessionStore();

onMounted(() => {
  if (sessionStore.session === null) {
    sessionStore.$reset() // reset the state only at client-side when the component is mounted to avoid hydration mismatches
  }
})