Mocking imports in Histoire for Nuxt 3

I’m currently working on a Nuxt 3 project and found myself wanting to use Histoire (the Vue/Vite ecosystem Storybook alternative) for the component documentation.

I’d abandoned the attempt earlier during development as Histoire wasn’t quite stable with Nuxt back in the autumn, but recent Histoire updates (v0.17.6 as at time of writing) seem to have fixed those legacy issues. However when I came to install it again, I found myself with a brand new error:

500 Context conflict

at checkConflict (http://localhost:6006/node_modules/.pnpm/[email protected]/node_modules/unctx/dist/index.mjs?v=161554df:6:13)
at Object.set (http://localhost:6006/node_modules/.pnpm/[email protected]/node_modules/unctx/dist/index.mjs?v=161554df:40:9)
at callWithNuxt (http://localhost:6006/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/nuxt/dist/app/nuxt.js?v=161554df:185:16)
at getRequestURLWN (http://localhost:6006/node_modules/.pnpm/@[email protected][email protected][email protected]/node_modules/@sidebase/nuxt-auth/dist/runtime/utils/callWithNuxt.mjs?v=161554df:4:42)
at getSession (http://localhost:6006/node_modules/.pnpm/@[email protected][email protected][email protected]/node_modules/@sidebase/nuxt-auth/dist/runtime/composables/authjs/useAuth.mjs?v=161554df:96:37)
at http://localhost:6006/node_modules/.pnpm/@[email protected][email protected][email protected]/node_modules/@sidebase/nuxt-auth/dist/runtime/plugin.mjs?v=161554df:17:46
at executeAsync (http://localhost:6006/node_modules/.pnpm/[email protected]/node_modules/unctx/dist/index.mjs?v=161554df:111:19)
at http://localhost:6006/node_modules/.pnpm/@[email protected][email protected][email protected]/node_modules/@sidebase/nuxt-auth/dist/runtime/plugin.mjs?v=161554df:17:27
at http://localhost:6006/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/nuxt/dist/app/nuxt.js?v=161554df:111:60
at fn (http://localhost:6006/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/nuxt/dist/app/nuxt.js?v=161554df:181:44)

I already knew from a Discord thread that other people were having similar issues, and that this was related to @sibebase/nuxt-auth. I started to dig in a little bit and follow this back through the code and it started to sort of make sense. The context conflict is triggered by unctx. What I think is happening is that Nuxt auth is trying to make callWithNuxt calls but fails, possibly due to these documented pitfalls. Not entirely sure why that would be, maybe because nuxt isn’t running in a “standard” way under histoire, maybe some conflict with histoire itself, maybe something somewhere is making assumptions that don’t hold up when running via the histoire nuxt plugin. Anyway, it’s borked so need to come up with a solution.

My first thought was that I don’t really need “real” auth to be running at all in a component library. Some ideas came to mind:

  1. Document Vue components only - i.e. leave nuxt out of histoire entirely and just load components with Vue only. Whether this is a suitable option will very much depend on how far along you are with the project and how much logic/state is shared with your components. I tried this on a simple component and it worked fine, however I had to set up all my asset imports again in histoire.setup.ts (e.g. css, images etc) and also had to manually install/import @vitejs/plugin-vue to get it to work. I also lost all the auto importing magic but I guess I could replicate it in histoire via an auto import vite plugin. In short, this solution might be ok up to a certain point, but I’ve got a whole bunch of less decoupled components where it definitely won’t be.

  2. Bypass Nuxt Auth completely in the histoire environment - I simply tried commenting out @sidebase/nuxt-auth in my nuxt.config.ts This also worked ok, obviously any component that touches auth in any way might not fare too well, and there were loads of console errors complaining about the missing #auth import but at least the component loaded in histoire. I thought that maybe we could have some kind of condition in the nuxt config, and then just ignore those console errors.

  3. Mock nuxt auth as you would do in a test environment. This seems to be a thing in the Storybook world. I found this github issue which gives some ideas on how to implement: e.g. this comment suggests using an auto import vite plugin to swap out modules in histoire config.

Mocks definitely seemed like the cleanest way forward to me.

My solution

So first we make a really basic module that mocks the #auth alias. It only needs to mock the specific functions that your app calls, and it only needs to return something where your app actually requires it. Just take care to follow the shape/types of the original library where relevant.

// stories/authMock.ts
const getServerSession = async () => {
    return Promise.resolve({
        user: {
            local: {
                id: 1
            }
        }
    });
}

const NuxtAuthHandler = () => {
    return
}

export {
    getServerSession,
    NuxtAuthHandler
}

You can put this anywhere you like, but I’ve put this in the stories dir just to keep things all together.

Then in our histoire.config.ts, we’re going to set a histoire flag for us to read in our nuxt config, so that we know we’re in histoire mode.

import { defineConfig } from 'histoire'
import { HstVue } from '@histoire/plugin-vue'
import { HstNuxt } from '@histoire/plugin-nuxt'

process.env.HISTOIRE = 'true'

export default defineConfig({
    plugins: [
        HstVue(),
        HstNuxt(),
    ]
})

Then in nuxt.config.ts we conditionally set modules and alias based on the presence of the flag. If it’s histoire we’re not going to load @sidebase/nuxt-auth and instead we’ll load our #auth mock alias.

import { resolve } from 'path'
let modules = [
    '@nuxtjs/tailwindcss',
    '@sidebase/nuxt-auth',
    'nuxt-icon',
    '@vue-email/nuxt',
    '@pinia/nuxt',
		// any other modules you're using
]

let alias = {} // empty default

if (process.env.HISTOIRE === 'true') {
    modules = [
        '@nuxtjs/tailwindcss',
        'nuxt-icon',
        '@vue-email/nuxt',
        '@pinia/nuxt',
				// no nuxt-auth here
    ]

    alias = {
        '#auth': resolve(__dirname, './stories/authMock.ts'),
    }
}

export default defineNuxtConfig({
	//... other settings
	modules,

	alias,

	//... other settings
})

That’s pretty much it - now when nuxt runs during histoire, it will use our mock #auth instead of loading @sidebase/nuxt-auth

Global Auto-Imports

What if we needed to mock an ad hoc auto-imported function rather than an alias? In that case I presume we should be able to use unplugin-auto-import as per that earlier github comment While that comment suggests it can be set up within histoire’s config for that use case, that may depend on when the function gets called/imported in your own app’s lifecycle - I certainly wasn’t able to configure the nuxt-auth vite alias within histoire.config.ts and had to do it within nuxt.config.ts