quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time
https://quasar.dev
MIT License
25.5k stars 3.45k forks source link

Storybook setup - Cannot set property '$q' of undefined #8607

Closed alvarosabu closed 2 years ago

alvarosabu commented 3 years ago

Describe the bug Cannot set property '$q' of undefined error on Object.installQuasar when trying to use Storybook/vue3

import {Quasar} from 'quasar';
import iconSet from 'quasar/icon-set/mdi-v4.js';
import lang from 'quasar/lang/de.js';
import '@quasar/extras/roboto-font/roboto-font.css';
import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
import '@quasar/extras/mdi-v4/mdi-v4.css';
import 'quasar/dist/quasar.css';
import { app } from '@storybook/vue3';

app.use(Quasar, {
  config: {},
  plugins: {},
  lang: lang,
  iconSet: iconSet
});

Codepen/jsFiddle/Codesandbox (required) https://github.com/alvarosaburido/quasar-v2-storyook-demo

To Reproduce Steps to reproduce the behavior:

  1. Clone git clone https://github.com/alvarosaburido/quasar-v2-storyook-demo
  2. Install deps
  3. Run yarn storybook
  4. See error on console

Expected behavior The standalone version of Quasar v2 using app.use(Quasar) works with Storybook and webpack

Screenshots

Screenshot 2021-03-10 at 20 13 54

Platform (please complete the following information): Quasar Version: 2.0.0-beta.9 @quasar/app Version: N/A Quasar mode: [ ] SPA [ ] SSR [ ] PWA [ ] Electron [ ] Cordova [ ] Capacitor [ ] BEX Tested on: [x ] SPA [ ] SSR [ ] PWA [ ] Electron [ ] Cordova [ ] Capacitor [ ] BEX OS: Node: NPM: Yarn: Browsers: iOS: Android: Electron:

Additional context Not sure if it's an issue with Quasar or with @storybook/vue3. If it's not a Quasar thing let me know to open a ticket on the proper place

alvarosabu commented 3 years ago

I might find why. Using the app coming from '@storybook/vue3' , its ssrContext is undefined, which causes the issue.

const installQuasar = __QUASAR_SSR_SERVER__
  ? function (app, opts = {}, ssrContext) {
      const $q = {
        version: __QUASAR_VERSION__,
        config: Object.freeze(opts.config || {})
      }

      ssrContext.$q = $q
Screenshot 2021-03-11 at 13 32 55

Possible fix, adding a default value for ssrContext as an empty obj.

const installQuasar = __QUASAR_SSR_SERVER__
  ? function (app, opts = {}, ssrContext = {})
alvarosabu commented 3 years ago

For some reason is treating Storybook as SSR. __QUASAR_SSR_SERVER__ is set to true Screenshot 2021-03-12 at 10 25 40

axe-me commented 3 years ago

any updates on this ? same thing happens in jest.

axe-me commented 3 years ago

took a look into the source code and built code, looks like the built code is only for SSR mode: https://github.com/quasarframework/quasar/blob/2.0.0-beta.11/ui/src/install-quasar.js#L70 this code is built into: image

maybe the ssr option should be a run time config, something like:

app.use(Quasar, { ssr: true });
axe-me commented 3 years ago

for now I have to use the umd version in jest:

moduleNameMapper: {
    "^quasar$": "quasar/dist/quasar.umd.prod.js",
},
andreasphil commented 3 years ago

For anyone coming across this issue while trying to set up Storybook + Vue 3 + Quasar 2 as a Vue CLI plugin, this is how I got it working:

Create a Vue CLI app, install the Quasar plugin and npx sb init --type=vue3 as usual. Then tell the Vue instance running in Storybook to use Quasar just like you would in your app's main.ts by adding the following to your .storybook/preview.js file:

import { app } from "@storybook/vue3";
import { Quasar } from "quasar";
import quasarUserOptions from "../src/quasar-user-options";

app.use(Quasar, quasarUserOptions);

By default, Storybook doesn't know how to handle the scss files imported in quasar-user-options and also won't be able to resolve the Quasar variable paths. I fixed this by adding the following config in .storybook/webpack.config.js:

module.exports = ({ config }) => {
  config.resolve.alias = {
    ...config.resolve.alias,
    "quasar-variables": "quasar/src/styles/quasar.variables.scss",
    "quasar-variables-styl": "quasar/src/css/variables.sass",
    "quasar-styl": "quasar/dist/quasar.sass",
    "quasar-addon-styl": "quasar/src/css/flex-addon.sass",
  };

  config.module.rules.push({
    test: /\.(scss|sass)$/,
    use: ["style-loader", "css-loader", "sass-loader"],
    include: path.resolve(__dirname, "../"),
  });

  return config;
};

I discovered the file paths by inspecting Vue's webpack config (vue-cli-service inspect). Apparently the CLI plugin adds some aliases so we can import cleaner file names. Only tested it with small components so far but it seems to work just fine. Hope that helps.

Thanks @alvarosaburido, your descriptions pointed me in the right direction!

alvarosabu commented 3 years ago

Hey @andreasphil . Eventually, I was able to make it work exactly as you did. Thanks for sharing

holtalanm commented 3 years ago

Ran into this issue when attempting to run unit tests on components using Quasar with vue-test-utils.

It thinks it is running in SSR mode, which I guess is kind of correct, but not, since that is not intended. I end up getting Cannot set property $q of undefined when it tries to app.use the plugin.

holtalanm commented 3 years ago

unfortunately using the workaround mentioned, for me, ends up with:

console.warn node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:40
    [Vue warn]: A plugin must either be a function or an object with an "install" function.

  console.warn node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:40
    [Vue warn]: Failed to resolve component: q-page
      at <HelloWorld ref="VTU_COMPONENT" >
      at <VTUROOT>

because it is expecting import {Quasar} from 'quasar' to be in cjs format, not umd format.

for reference, this is the workaround attempted:

for now I have to use the umd version in jest:

moduleNameMapper: {
    "^quasar$": "quasar/dist/quasar.umd.prod.js",
},
holtalanm commented 3 years ago

for anyone coming to this with the same issue I had (unit testing with jest), I was able to make it work as follows:

in jest.config.js:

  moduleNameMapper: {
    '^quasar$': 'quasar/dist/quasar.esm.prod.js'
  },

then i created a Quasar helper file, quasarSetup.ts:

import {config} from '@vue/test-utils'
import quasarUserOptions from '@/quasar-user-options.js'
import {Quasar} from 'quasar'
import {DefineComponent} from 'vue'

config.global.plugins.push([
    Quasar, 
    quasarUserOptions
])

export function wrapInLayout<C extends DefineComponent<{}, {}, any>>(component: C) {
    return {
        template: `
            <q-layout view="lHh Lpr lFf">
                <UnderTest v-bind="$attrs"/>
            </q-layout>
        `,
        components: {
            UnderTest: component
        }
    }
}

then, usage example.spec.ts:

import {mount} from '@vue/test-utils'
import {wrapInLayout} from '@/../tests/unit/imports/quasarSetup'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('renders', () => {
    const wrapper = mount(wrapInLayout(HelloWorld))
    const img = wrapper.find('img')
    expect(img.attributes('alt')).toMatch('Quasar logo')
  })
})

if you're using an icon set, you may have to jest.mock any css files from those icon sets, but this got me going. Hope it helps!

Trinovantes commented 3 years ago

I was able to get quasar-beta14 to run on my custom SSR setup with this:

// app.ts
import { SSRContext } from '@vue/server-renderer'
import { createSSRApp } from 'vue'
import App from './client/components/App.vue'

export type AppContext = SSRContext & {
    // Needed by quasar
    // Must match https://github.com/quasarframework/quasar/blob/c5a527b80de03fc40e03ab66f0a7afe3651a8fa7/ssr-helpers/create-renderer.js#L152-L158
    _modules: Set<unknown>
    _meta: Record<string, unknown>
    onRendered: (fn: unknown) => void
}

export function createApp(ssrContext?: AppContext) {
    const app = createSSRApp(App)
    app.use(Quasar, {}, ssrContext) // Key part is this 3rd arg
    return { app }
}

// www.ts
express.use('*', (req, res, next) => {
    const onRenderedList: Array<unknown> = []
    const ssrContext: AppContext = {
        _modules: new Set(),
        _meta: {},
        onRendered: (fn) => { onRenderedList.push(fn) }, // No idea what this is used for
    }
    const { app } = createApp(ssrContext)
})

For anyone looking for Vue2/Quasar1 solution:

// app.ts
import App from './client/components/App.vue'

export function createApp(ssrContext?: AppContext) {
    const router = createRouter()
    const store = createStore()

    const appOptions: ComponentOptions<Vue> = {
        router: router,
        store: store,
        render: (createElement: CreateElement) => {
            return createElement(App)
        },
    }

    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
    Quasar.ssrUpdate({
        app: appOptions,
        ssr: ssrContext,
    })

    const app = new Vue(appOptions)
    return { app, router, store }
}

I personally think it's annoying that Quasar has so many undocumented global side effects and requires digging through their source code to fix these niche use cases

rasenfer commented 2 years ago

I can fixed it following @andreasphil solution https://github.com/quasarframework/quasar/issues/8607#issuecomment-813328197 whit some changes.

I have my project created with vue add quasar and vue add storybook (recent versions at 200-09-28) and keep failing with this solution.

hawkeye64 commented 2 years ago

As there have been several workarounds to everyone's issues, I am closing this issue. Feel free to re-open if there are still concerns.

OfekA-IAI commented 2 years ago

Here is my Storybook + Quasar v2 configuration that worked with .sass variables:

// preview.js
import "@quasar/extras/roboto-font/roboto-font.css";
import "@quasar/extras/material-icons/material-icons.css";
import "@quasar/extras/animate/fadeInUp.css";
import "@quasar/extras/animate/fadeOutDown.css";
import "@quasar/extras/animate/fadeInRight.css";
import "@quasar/extras/animate/fadeOutRight.css";

import "quasar/dist/quasar.sass";

import "../src/assets/css/global.scss";

import { app } from "@storybook/vue3";
import { Quasar } from "quasar";

app.use(Quasar, {});

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};
// main.js
const path = require("path");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
  framework: "@storybook/vue3",

  webpackFinal: async (config, { configType }) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        "style-loader",
        "css-loader",
        {
          loader: "sass-loader",
          options: {
            additionalData: `
          @import './src/assets/css/imported/_variables.scss';
        `,
          },
        },
      ],
    });

    config.module.rules.push({
      test: /\.sass$/,
      use: [
        "style-loader",
        "css-loader",
        {
          loader: "sass-loader",
          options: {
            additionalData: `
            @import './src/assets/css/quasar-variables.sass'
            @import 'quasar/src/css/variables.sass'
          `,
          },
        },
      ],
    });

    // Return the altered config
    return config;
  },
};