Skip to content

Commit

Permalink
feat: start storybook in same process as dev server (#592)
Browse files Browse the repository at this point in the history
Instead of invoking the storybook cli, we now start the storybook dev
server in the same process as the nuxt dev server.
This has the advantage that we don't need to spawn a second nuxt
instance, improving preformance and reducing complexifty.

Fixes storybook-vue/storybook-nuxt#59 and
fixes #610 and fixes
#635 and fixes
#475.
  • Loading branch information
tobiasdiez authored Jun 16, 2024
1 parent a816e6e commit 1359f01
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 577 deletions.
11 changes: 11 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
externals: [
// Workaround for https://github.com/sindresorhus/globby/issues/260
'globby',
// Esbuild cannot be bundled
'esbuild',
],
// Ignore warnings
failOnWarn: false,
})
3 changes: 3 additions & 0 deletions examples/showcase/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['@nuxtjs/storybook'],
vue: {
runtimeCompiler: true,
},
})
2 changes: 2 additions & 0 deletions packages/storybook-nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@
"@storybook/builder-vite": "^8.0.0",
"@storybook/vue3": "^8.0.0",
"@storybook/vue3-vite": "^8.0.0",
"json-stable-stringify": "^1.1.1",
"nuxt": "^3.11.1",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@storybook/types": "^8.0.0",
"@types/json-stable-stringify": "^1.0.36",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"changelogen": "^0.5.5",
Expand Down
175 changes: 105 additions & 70 deletions packages/storybook-nuxt/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import vuePlugin from '@vitejs/plugin-vue'
import replace from '@rollup/plugin-replace'
import type { StorybookConfig } from './types'
import { componentsDir, composablesDir, pluginsDir, runtimeDir } from './dirs'
import stringify from 'json-stable-stringify'

const packageDir = resolve(fileURLToPath(import.meta.url), '../..')
const distDir = resolve(fileURLToPath(import.meta.url), '../..', 'dist')

const dirs = [distDir, packageDir, pluginsDir, componentsDir]

let nuxt: Nuxt

/**
* extend nuxt-link component to use storybook router
* @param nuxt
Expand Down Expand Up @@ -55,16 +54,29 @@ async function extendComposables(nuxt: Nuxt) {
})
}

async function defineNuxtConfig(baseConfig: {
root: string | undefined
plugins: { name: string }[]
}) {
const { loadNuxt, buildNuxt, addPlugin, extendPages } = await import(
'@nuxt/kit'
)
async function loadNuxtViteConfig(root: string | undefined) {
const { loadNuxt, tryUseNuxt, buildNuxt, addPlugin, extendPages } =
await import('@nuxt/kit')

let nuxt = tryUseNuxt()
if (nuxt) {
// Nuxt is already started in the current process (i.e. in dev mode)
// We assume that we are called from the Nuxt module, which means that
// Nuxt is in the "load module" state and we can access the Vite config later via the hook
const nuxtRes = nuxt
return new Promise<{ viteConfig: ViteConfig; nuxt: Nuxt }>((resolve) => {
nuxtRes.hook('vite:configResolved', (config, { isClient }) => {
if (isClient) {
resolve({
viteConfig: config,
nuxt: nuxtRes,
})
}
})
})
}
nuxt = await loadNuxt({
cwd: baseConfig.root,
cwd: root,
ready: false,
dev: false,
overrides: {
Expand All @@ -78,8 +90,6 @@ async function defineNuxtConfig(baseConfig: {
throw new Error(
`Storybook-Nuxt does not support '${nuxt.options.builder}' for now.`,
)

let extendedConfig: ViteConfig = {}
nuxt.options.build.transpile.push(join(packageDir, 'preview'))

nuxt.hook('modules:done', () => {
Expand Down Expand Up @@ -107,29 +117,8 @@ async function defineNuxtConfig(baseConfig: {
(resolve, reject) => {
nuxt.hook('vite:configResolved', (config, { isClient }) => {
if (isClient) {
extendedConfig = mergeConfig(config, baseConfig)

const plugins = extendedConfig.plugins || []

// Find the index of the plugin with name 'vite:vue'
const index = plugins.findIndex(
(plugin) =>
plugin && 'name' in plugin && plugin.name === 'vite:vue',
)

// Check if the plugin was found
if (index !== -1) {
// Replace the plugin with the new one using vuePlugin()
plugins[index] = vuePlugin()
} else {
// Vue plugin should be the first registered user plugin so that it will be added directly after Vite's core plugins
// and transforms global vue components before nuxt:components:imports.
plugins.unshift(vuePlugin())
}

extendedConfig.plugins = plugins
resolve({
viteConfig: extendedConfig,
viteConfig: config,
nuxt,
})
// Stop the build process, as we don't need to build the Nuxt app
Expand All @@ -145,6 +134,68 @@ async function defineNuxtConfig(baseConfig: {
},
).finally(() => nuxt.close())
}

function mergeViteConfig(
storybookConfig: ViteConfig,
nuxtConfig: ViteConfig,
nuxt: Nuxt,
): ViteConfig {
const extendedConfig: ViteConfig = mergeConfig(nuxtConfig, storybookConfig)

const plugins = extendedConfig.plugins || []

// Find the index of the plugin with name 'vite:vue'
const index = plugins.findIndex(
(plugin) => plugin && 'name' in plugin && plugin.name === 'vite:vue',
)

// Check if the plugin was found
if (index !== -1) {
// Replace the plugin with the new one using vuePlugin()
plugins[index] = vuePlugin()
} else {
// Vue plugin should be the first registered user plugin so that it will be added directly after Vite's core plugins
// and transforms global vue components before nuxt:components:imports.
plugins.unshift(vuePlugin())
}

extendedConfig.plugins = plugins
// Storybook adds 'vue' as dependency that should be optimized, but nuxt explicitly excludes it from pre-bundling
// Prioritize `optimizeDeps.exclude`. If same dep is in `include` and `exclude`, remove it from `include`
extendedConfig.optimizeDeps!.include =
extendedConfig.optimizeDeps!.include!.filter(
(dep) => !extendedConfig.optimizeDeps!.exclude!.includes(dep),
)
return mergeConfig(extendedConfig, {
// build: { rollupOptions: { external: ['vue', 'vue-demi'] } },
define: {
__NUXT__: JSON.stringify({
config: nuxt.options.runtimeConfig,
}),
'import.meta.client': 'true',
},

plugins: [
replace({
values: {
'import.meta.server': 'false',
'import.meta.client': 'true',
},
preventAssignment: true,
}),
],
server: {
cors: true,
proxy: {
...getPreviewProxy(),
...getNuxtProxyConfig(nuxt).proxy,
},
fs: { allow: [searchForWorkspaceRoot(process.cwd()), ...dirs] },
},
envPrefix: ['NUXT_'],
})
}

export const core: PresetProperty<'core', StorybookConfig> = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config: any,
Expand Down Expand Up @@ -183,44 +234,28 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (
if (!ViteFile) throw new Error('ViteFile not found')
return ViteFile(c, o)
}
const nuxtConfig = await defineNuxtConfig(
await getStorybookViteConfig(config, options),
const storybookViteConfig = await getStorybookViteConfig(config, options)
const { viteConfig: nuxtConfig, nuxt } = await loadNuxtViteConfig(
storybookViteConfig.root,
)
const finalViteConfig = mergeViteConfig(config, nuxtConfig, nuxt)
// Write all vite configs to logs
const fs = await import('node:fs')
fs.mkdirSync(join(options.outputDir, 'logs'), { recursive: true })
fs.writeFileSync(
join(options.outputDir, 'logs', 'vite-storybook.config.js'),
stringify(storybookViteConfig, { space: ' ' }),
)
fs.writeFileSync(
join(options.outputDir, 'logs', 'vite-nuxt.config.js'),
stringify(nuxtConfig, { space: ' ' }),
)
fs.writeFileSync(
join(options.outputDir, 'logs', 'vite-final.config.js'),
stringify(finalViteConfig, { space: ' ' }),
)
// Storybook adds 'vue' as dependency that should be optimized, but nuxt explicitly excludes it from pre-bundling
// Prioritize `optimizeDeps.exclude`. If same dep is in `include` and `exclude`, remove it from `include`
nuxtConfig.viteConfig.optimizeDeps!.include =
nuxtConfig.viteConfig.optimizeDeps!.include!.filter(
(dep) => !nuxtConfig.viteConfig.optimizeDeps!.exclude!.includes(dep),
)

return mergeConfig(nuxtConfig.viteConfig, {
// build: { rollupOptions: { external: ['vue', 'vue-demi'] } },
define: {
__NUXT__: JSON.stringify({
config: nuxtConfig.nuxt.options.runtimeConfig,
}),
'import.meta.client': 'true',
},

plugins: [
replace({
values: {
'import.meta.server': 'false',
'import.meta.client': 'true',
},
preventAssignment: true,
}),
],
server: {
cors: true,
proxy: {
...getPreviewProxy(),
...getNuxtProxyConfig(nuxt).proxy,
},
fs: { allow: [searchForWorkspaceRoot(process.cwd()), ...dirs] },
},
envPrefix: ['NUXT_'],
})
return finalViteConfig
}

async function getPackageDir(frameworkPackageName: string) {
Expand Down
10 changes: 2 additions & 8 deletions packages/storybook-nuxt/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ document.body.appendChild(vueAppRootContainer)
// entry()
const logger = console
async function nuxtAppEntry() {
const nuxtApp = () =>
import('#app/entry').then((m) => m.default).catch(() => {})
// i
const vueAppPromise = nuxtApp().catch((_error) => {
// consola.error('Error while mounting app:', error)
})
return vueAppPromise
const nuxtApp = () => import('#app/entry').then((m) => m.default)
return nuxtApp()
}

nuxtAppEntry().then((app) => {
logger.log('nuxtAppEntry done', app)
// @ts-expect-error: void should never be returned (fix this in the future)
app()
.then(() => {
logger.log('nuxtAppEntry app done')
Expand Down
Loading

0 comments on commit 1359f01

Please sign in to comment.