New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide information how to use Quasar with Storybook #11654
Comments
What is the error message you are facing? Maybe this issue #11683? |
The error message is And I don't think it is related to the issue you tagged |
Having the same issue, as a workaround, I re-imported it in my component using variables: <style lang="scss">
// I have to add it for Storybook to avoid SassError: Undefined variable. background-color: rgba($primary-200, 0.8);
// TODO Configure Storybook properly
@import "src/css/quasar.variables.scss";
... I'll let you know, if I find a good config. |
We must use css variables as explained here https://quasar.dev/style/color-palette#dynamic-change-of-brand-colors-dynamic-theme-colors- @badsaarow replied here giving an example badsaarow/quasar2-storybook-boilerplate#47 (comment) |
@jclaveau what is the correct way to set the spacing variables? I'm struggling with how to transfer this file to storybook |
I have Quasar + Vite + Storybook running with the following preview.ts: import type { Preview } from '@storybook/vue3';
import '@quasar/extras/mdi-v7/mdi-v7.css';
import 'quasar/dist/quasar.css';
import { setup } from '@storybook/vue3';
import { Quasar } from 'quasar';
setup((app) => {
app.use(Quasar, {});
});
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview; This works, except my custom styles from app.sass and quasar.variables.sass won't get loaded. |
A way to use Storybook 7 with Quasar / Vite and SCSS supportUse Vite 4
npm remove @intlify/vite-plugin-vue-i18n # outdated
npm install @quasar/app-vite@v2.0.0-alpha.11
npm update @quasar/vite-plugin
npm update @vitejs/plugin-vue
Install Storybook
Extract the Vite config generated by Quasar (based on quasar.config.js) and inject it into Storybook's Vite
import { QuasarConfFile } from '@quasar/app-vite/lib/quasar-config-file'
import { getQuasarCtx } from '@quasar/app-vite/lib/utils/get-quasar-ctx'
import { extensionRunner } from '@quasar/app-vite/lib/app-extension/extensions-runner'
// This code is taken from @quasar/app/bin/quasar-inspect
export async function getQuasarConfig (mode='spa', debug=true, cmd='dev', hostname=9000, port=9000) {
// Requires adding
// // https://github.com/evanw/esbuild/issues/1921#issuecomment-1197938703
// + "\nimport { createRequire } from 'module';const require = createRequire(import.meta.url);"
// to apps/quasar/node_modules/@quasar/app-vite/lib/quasar-config-file.js / createEsbuildConfig () / esbuilConfig.banner.js
const ctx = getQuasarCtx({
mode: mode,
target: mode === 'cordova' || mode === 'capacitor'
? 'android'
: void 0,
debug: debug,
dev: cmd === 'dev',
prod: cmd === 'build'
// vueDevtools?
})
await extensionRunner.registerExtensions(ctx)
const quasarConfFile = new QuasarConfFile({
ctx,
port: port,
host: hostname,
watch: undefined
})
const quasarConf = await quasarConfFile.read()
if (quasarConf.error !== void 0) {
throw new Error(quasarConf.error)
}
// console.log('quasarConf', quasarConf)
const modeConfig = (await import(`@quasar/app-vite/lib/modes/${mode}/${mode}-config.js`))?.modeConfig
const cfgEntries = []
let threadList = Object.keys(modeConfig)
for (const name of threadList) {
cfgEntries.push({
name,
object: await modeConfig[ name ](quasarConf)
})
}
return cfgEntries
}
diff --git a/node_modules/@quasar/app-vite/lib/quasar-config-file.js b/node_modules/@quasar/app-vite/lib/quasar-config-file.js
index 904363d..9645fa6 100644
--- a/node_modules/@quasar/app-vite/lib/quasar-config-file.js
+++ b/node_modules/@quasar/app-vite/lib/quasar-config-file.js
@@ -67,12 +67,28 @@ function createEsbuildConfig () {
},
banner: {
js: quasarConfigBanner
+ // https://github.com/evanw/esbuild/issues/1921#issuecomment-1197938703
+ + `
+ // This is probably due to the fact we use the alpha version of this package.
+ // TODO remove it once it is released
+ import { createRequire } from 'node:module';
+ const cjsRequire = createRequire(import.meta.url);
+ const require = (path) => {
+ const matches = path.match(/^(.+)\.mjs$/)
+
+ if (matches !== null) {
+ path = matches[1] + '.js'
+ }
+
+ return cjsRequire(path);
+ }
+ `
},
define: quasarEsbuildInjectReplacementsDefine,
resolveExtensions: [ appPaths.quasarConfigOutputFormat === 'esm' ? '.mjs' : '.cjs', '.js', '.mts', '.ts', '.json' ],
entryPoints: [ appPaths.quasarConfigFilename ],
outfile: tempFile,
- plugins: [ quasarEsbuildInjectReplacementsPlugin ]
+ plugins: [ quasarEsbuildInjectReplacementsPlugin ],
}
}
import type { StorybookConfig } from '@storybook/vue3-vite';
import type { Options } from '@storybook/types';
import type { InlineConfig, PluginOption } from 'vite';
import { mergeConfig } from 'vite'
import { getQuasarConfig } from './quasar-config-result';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
async viteFinal(config: InlineConfig, options: Options): Promise<Record<string, any>> {
// https://github.com/storybookjs/builder-vite#migration-from-webpack--cra
const quasarConfig = await getQuasarConfig()
const quasarViteConfig = quasarConfig[0].object
// console.log('quasarConfig', quasarViteConfig.server)
// console.log('storybook Vite config', JSON.stringify(config, null, 2))
// Quasar's Vite Plugins
const quasarVitePluginNames = quasarViteConfig.plugins.map((pluginConfig: PluginOption) => {
if (pluginConfig instanceof Promise) {
throw new Error('Promise is not supported for Quasar\'s vite config merge')
}
if (Array.isArray(pluginConfig)) {
// TODO are these config required for stories rendering?
console.warn('Arrays of Vite PluginOption are not supported for Quasar\'s vite config merge', JSON.stringify(pluginConfig, null, 2))
}
else if (pluginConfig !== false) {
return pluginConfig.name
}
})
// We must remove Vue plugins from Storybook before injecting Quasar's ones
config.plugins = config.plugins.filter((pluginConfig: PluginOption) => {
if (pluginConfig instanceof Promise) {
throw new Error('Promise is not supported for Quasar\'s vite config merge')
}
if (pluginConfig instanceof Array) {
throw new Error('Arrays of Vite PluginOption are not supported for Quasar\'s vite config merge')
}
return !pluginConfig || pluginConfig.name == null || ! quasarVitePluginNames.includes(pluginConfig.name)
})
config.plugins = [...config.plugins, ...quasarViteConfig.plugins]
const updatedConfig: Record<string, any> = mergeConfig(config, {
resolve: {
alias: {
...quasarViteConfig.resolve.alias
},
},
server: {
...quasarViteConfig.server
},
// Avoid error Failed to load url /sb-preview/runtime.js (resolved id: /sb-preview/runtime.js). Does the file exist?
// [vite]: Rollup failed to resolve import "/./sb-preview/runtime.js" from "/home/jean/dev/Hippocast/prototype/apps/quasar/iframe.html".
// This is most likely unintended because it can break your application at runtime.
// If you do want to externalize this module explicitly add it to
// `build.rollupOptions.external`
build: {
rollupOptions: {
external: [
/sb-preview\/runtime.js$/,
]
}
}
});
return updatedConfig
},
};
export default config; Boot Storybook's Vue app like the Quasar one
/**
* This file is a copy of .quasar/client-entry.js Vue app object injected.
* As client-entry.js is generated from template, you may have to patch it
* if you change quasar.conf.js
*/
// When quasar.config.js changes, css files must be manually imported here as in apps/quasar/.quasar/client-entry.js
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
// We load Quasar stylesheet file
import 'quasar/dist/quasar.sass'
import 'src/css/app.scss'
console.info('[Quasar] Running SPA for Storybook.')
const publicPath = ``
export async function start ({ app, router, store }, bootFiles) {
let hasRedirected = false
const getRedirectUrl = url => {
try { return router.resolve(url).href }
catch (err) {}
return Object(url) === url
? null
: url
}
const redirect = url => {
hasRedirected = true
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
window.location.href = url
return
}
const href = getRedirectUrl(url)
// continue if we didn't fail to resolve the url
if (href !== null) {
window.location.href = href
// window.location.reload()
}
}
const urlPath = window.location.href.replace(window.location.origin, '')
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
try {
await bootFiles[i]({
app,
router,
store,
ssrContext: null,
redirect,
urlPath,
publicPath
})
}
catch (err) {
if (err && err.url) {
redirect(err.url)
return
}
console.error('[Quasar] boot error:', err)
return
}
}
if (hasRedirected === true) {
return
}
// App mounting is handled by Storybook
// app.mount('#q-app')
}
import type { Preview } from '@storybook/vue3';
import { setup } from '@storybook/vue3';
import type { App } from 'vue';
// Customize Storybook's style
import './storybook.scss'
import quasarUserOptions from '../.quasar/quasar-user-options' // lang / iconset
import { start } from './client-entry-storybook.js'
type ModuleType = {
default: null | Function
}
import createStore from '../src/stores/index'
import createRouter from '../src/router/index'
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
(async () => {
// Chromatic doesn't support top level awaits
// Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)
// The store and router instanciation must be done here as Storybook's setup() fn doesn't support async
const store = typeof createStore === 'function'
? await createStore({})
: createStore
// Those are required by createRouter()
global.process = {
env: {
VUE_ROUTER_MODE: 'hash', // Use a routing through hash to avoid ovewriting Storybook url parts (e.g. http://192.168.1.8:6006/iframe.html?globals=backgrounds.grid:!true;backgrounds.value:!hex(F8F8F8)&args=#/)
VUE_ROUTER_BASE: '/iframe.html', // The url of Storybook's preview iframe
},
}
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
setup((app: App) => {
app.use(Quasar, quasarUserOptions)
app.use(store)
store.use(({ store }) => { store.router = router })
// router must be used before boot files to avoid "[Vue warn]: Failed to resolve component: router-view" in SB
// TODO ensure this works always. Does vueRouterMode: 'history' in config impact it?
app.use(router)
return Promise.all([
// When quasar.config.js changes, boot files must be manually imported here as in apps/quasar/.quasar/client-entry.js
// import('../src/boot/i18n'),
// import('boot/axios'),
])
.then((bootFiles: ModuleType[]) => {
const boot = bootFiles
.map((entry: ModuleType) => {
return entry.default
})
.filter(entry => entry instanceof Function)
start({app, router, store}, boot)
})
});
})()
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
// This makes SB stories still visible in case of js exception
.sb-wrapper {
position: relative !important;
max-height: 300px;
overflow-y: auto;
}
// Avoids infinite width producing errors on Chromatic due to the 25 millions pixels threshold passed
.fixed, .fixed-full, .fullscreen, .fixed-center, .fixed-bottom, .fixed-left, .fixed-right, .fixed-top, .fixed-top-left, .fixed-top-right, .fixed-bottom-left, .fixed-bottom-right {
position: absolute !important;
}
.storybook-anti-oversize-wrapper {
max-width: 2000px;
overflow-y: auto;
border: 1px solid #888;
} Add some sample stories
import type { Meta, StoryObj } from '@storybook/vue3';
import EssentialLink from '../components/EssentialLink.vue';
// More on how to set up stories at: https://storybook.js.org/docs/vue/writing-stories/introduction
const meta = {
title: 'Example/EssentialLink',
component: EssentialLink,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/vue/writing-docs/autodocs
tags: ['autodocs'],
argTypes: {
iconColor: { control: 'select', options: ['primary', 'secondary', 'accent'] },
// onClick: { action: 'clicked' },
},
args: { iconColor: 'accent' }, // default value
} satisfies Meta<typeof EssentialLink>;
export default meta;
type Story = StoryObj<typeof meta>;
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/vue/api/csf
* to learn how to use render functions.
*/
export const Simple: Story = {
args: {
// iconColor: 'primary',
title: 'My essential link',
caption: 'caption',
link: 'https://www.google.com',
icon: 'warning',
},
};
export const LongText: Story = {
args: {
title: 'Aenean neque urna, aliquam in nunc ac, volutpat finibus lectus. Etiam quis orci ut est blandit vestibulum id ut mauris. Cras consequat erat in elit convallis tempor. Duis quis nibh accumsan nibh congue vestibulum sed et nisl. Aliquam imperdiet suscipit magna, a vulputate lorem facilisis vel. Donec facilisis vehicula suscipit. Donec scelerisque vel sapien et posuere. Nunc sit amet lacinia metus. Vivamus egestas nulla in lectus fermentum varius. ',
caption: 'caption',
link: 'https://www.google.com',
icon: 'warning',
},
};
<template>
<q-item
clickable
tag="a"
target="_blank"
:href="link"
>
<q-item-section
v-if="icon"
avatar
>
<q-icon :name="icon" :color="iconColor"/>
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
export interface EssentialLinkProps {
title: string;
caption?: string;
link?: string;
icon?: string;
iconColor?: string;
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',
link: '#',
icon: '',
iconColor: '',
});
</script>
import type { Meta, StoryObj } from '@storybook/vue3';
import { useArgs } from '@storybook/preview-api';
import App from '../App.vue';
// This is definitelly not the best way to add custom args
// TODO investigate https://stackoverflow.com/a/72223811/2714285 and find a proper way to do this
type AppArgs = {
route: string;
};
type InputPropOverrides = {
args: AppArgs;
};
const meta = {
title: 'App',
component: App,
args: {
route: '/',
},
render: (args) => {
const [_, updateArgs] = useArgs();
window.location.hash = args.route
return {
components: { App },
data: () => {
return { args }
},
watch:{
$route (to) {
if (to.fullPath != args.route) {
updateArgs({ route: to.fullPath })
}
},
},
template: `
<div class="storybook-anti-oversize-wrapper">
<App />
</div>
`,
}
},
} satisfies Meta<typeof App> & InputPropOverrides;
export default meta;
type Story = StoryObj<typeof meta>;
export const AppSimple: Story & InputPropOverrides = {
args: {
route: '/',
},
}; Track .quasar changes to update Storybook config if needed
Configure Chromatic
name: 'Chromatic Publish'
# https://github.com/chromaui/action
# https://www.chromatic.com/docs/github-actions
on: pull_request # Avoids passing Chromatic's snapshots threshold
# https://stackoverflow.com/a/72408109/2714285
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
# Disable chromatic push during Renovate's updates
if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]' }}
defaults:
run:
working-directory: apps/quasar
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Required to retrieve git history
# https://github.com/renovatebot/renovate/issues/7716#issuecomment-734391360
- name: Read Node.js version from .nvmrc
run: echo "NODE_VERSION=$(cat ../../.nvmrc)" >> $GITHUB_OUTPUT
id: nvm
- name: Dump Node.js version from .nvmrc
run: echo "NODE_VERSION=$(cat ../../.nvmrc)"
# https://github.com/actions/setup-node#caching-global-packages-data
- uses: actions/setup-node@v3
with:
# https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#multiple-operating-systems-and-architectures
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
- name: npm install
run: npm install
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
workingDir: apps/quasar
"scripts": {
// ...
"chromatic": "npx chromatic --exit-zero-on-changes",
"postinstall": "patch-package"
}, ConclusionThis way I'm able to use Storybook 7 with Quasar 2.12.1 and Typescript but
Hoping supporting Storybook from the CLI (Webpack and Quasar) as a full featured component driven dev stack or Histoire for a lighter solution would interest the Quasar Team. Hoping those inputs will help you! |
Btw there are some related discussions |
@jclaveau where do you get I'm currently adding storybook to my project but its failing on that part: |
I installed quasar as a dev dependency of my project instead of globally (using pnpm this produces no disk space issue). Btw, just to warn you : Storybook 7 vite plugin has an issue as it calls the setup() hook and the Vue mount() function in parallel (at least a month ago it was like that). This produces some issues as the app is mounted before the Quasar boot files are fully run. So if your components use boot files like i18n or, in my case, a boot file to set components default properties values, you will have some troubles. I hope I will have some time to find a solution for it but for bow I'm blocked. |
Thank you @jclaveau for all the efforts that you've put into this. I've recently stumbled upon this issue of providing scss variables that would override default quasar values. And it's stopping me from implementing sb integration for our project's component library. I'll be definitely trying out your solution in the upcoming days, otherwise I'll have to resort to implementing our own mini-version of sb. Thanks again! To the quasar devs that might see this issue - I think it'd be really great for the otherwise awesome and complete quasar ecosystem to have an out-of-box storybook integration. |
Maybe Histoire would be easier ton integrate https://histoire.dev. If I remember well, there is an opened issue concerning Quasar support |
@jclaveau I have been able to get it running for a few of my pages and the main two hurdles that are left have just felt impossible to resolve. Mainly if any page uses $q variable for example $q.notify will give you a error: And the other main problem I ran into was the routing. I was able to resolve this mostly by adding:
In my main.ts storybook file, but this only works if I have full paths on all my pages, meaning an import like: Then storybook stops complaining but thats not realistic for all cases since sometimes I will have dynamic paths. Have you maybe found a way around these problems? |
It looks a lot like the issue due to unmounted boot files. This said, this change may have fixed in https://github.com/storybookjs/storybook/pull/23772/files#diff-761b3fb7b014b2341fea7300ea1308e11425ab0aeef90cbc5e0fa5a9acab0b5fR30
Interesting, I personally avoid using aliases.
I personally avoid dynamic paths and aliases as it makes me loose too much time with failing intellisense.
If I were you I would begin by updating Storybook to check if Quasar boot files work now as expected |
I got Quasar with Storybook working. Here's detail. I've read through a few sources:
The JavaScript in Plain English (Medium) post got Quasar components to render but I was having trouble rendering my own because I ran into Example unplugin-auto-import/vite with storybookCAUTION! This is a partial example, just pointing out how import AutoImport from 'unplugin-auto-import/vite';
const config: StorybookConfig = {
async viteFinal(config) {
if (!config.plugins) {
config.plugins = [];
}
config.plugins.push(
AutoImport({
imports: [
'vue',
],
dts: 'src/auto-imports.d.ts',
}),
); Thank you to all who are contributing to this feature! If I were starting fresh I would look at https://github.com/tasynguyen3894/demo_quasar_storybook_i18n_pinia/ and the related blog post and then make sure relevant vite plugins are loaded. |
Is your feature request related to a problem? Please describe.
For the past few weeks, I tried multiple times to make my Quasar project work with storybook, to no avail.
It works when no sass variables are used, but storybook fails to compile when it detects that sass variables are used anywhere, with an error about the variable being undefined.
I tried defining them and anything else i could think of, use various webpack sass loaders, import the variables from quasar, and other things that i no longer remember, none worked.
Describe the solution you'd like
Any solution to how I can use Quasar with storybook properly.
Describe alternatives you've considered
There are no alternatives to storybook that i'm aware of.
Storybook is an important of the project's build process.
Additional context
I created a reproduction repo of the error i'm getting, based on the Quasar 2 + storybook repo that someone built but fails with the same error when using sass variables.
Edit to be clear:
The error message is
SassError: Undefined variable.
Reproducttion:
https://github.com/Seanitzel/Quasar-V2-Storybook
The text was updated successfully, but these errors were encountered: