Skip to content

Nuxt plugin can make type-aware ESLint fail #851

Description

@costleya

Describe the bug

Running Histoire with @histoire/plugin-nuxt can make type-aware ESLint fail on valid Nuxt application code even though no application source or lint configuration changed.

After normal Nuxt preparation, ESLint resolves auto-imports such as definePageMeta, useNuxtApp, and useState, and lint passes. After histoire build, the same lint command reports @typescript-eslint/no-unsafe-* errors because those auto-imports can no longer be resolved from the generated declarations. Running normal Nuxt preparation again makes the errors disappear.

The result therefore depends on command order rather than source code:

nuxt prepare   -> lint passes
histoire build -> lint fails
nuxt prepare   -> lint passes again

This is a problem because .nuxt is part of the type environment consumed by type-aware ESLint. A successful Histoire command changes that environment for later application commands, so the same commit can pass or fail lint depending on which command ran first. This can produce CI failures, break local pre-commit lint after using Histoire, and report cascades of unsafe operations in unchanged application files.

@histoire/plugin-nuxt creates an auxiliary Nuxt instance for story rendering. That instance intentionally disables pages and replaces useNuxtApp with a Histoire-specific implementation, but it writes its generated declarations into the host application's normal .nuxt directory. Histoire's declarations remain there after Histoire exits, so subsequent application lint commands consume the wrong generated type environment.

Expected behavior: running Histoire does not change the result of a subsequent application lint when no application source or configuration changed. The auxiliary Nuxt instance should use an isolated build directory or otherwise preserve the host application's generated state.

The following application-side workaround prevents the failure:

export default defineNuxtConfig({
  buildDir: process.env.HISTOIRE === 'true' ? '.nuxt/histoire' : '.nuxt',
})

Histoire sets process.env.HISTOIRE to 'true'. With this workaround, Histoire writes under .nuxt/histoire, the application's .nuxt/imports.d.ts remains unchanged, and lint still passes immediately after the Histoire build.

Reproduction

https://github.com/costleya/histoire-nuxt-build-dir-repro

The repository contains one Nuxt page using definePageMeta, one Histoire story, and one type-aware ESLint rule. It does not contain the build-directory workaround.

Run:

pnpm install --frozen-lockfile
pnpm repro

The reproduction script verifies this sequence:

pnpm prepare
pnpm lint
pnpm story:build
pnpm lint

The first lint succeeds. Histoire successfully builds one story. The second lint then fails on the unchanged definePageMeta() call:
Unsafe call of a type that could not be resolved @typescript-eslint/no-unsafe-call
The script exits successfully only after confirming both the generated declaration change and this exact lint failure.
Before Histoire runs, .nuxt/imports.d.ts contains the normal application declarations, including:

export { useNuxtApp, tryUseNuxtApp, defineNuxtPlugin, definePayloadPlugin, useRuntimeConfig, defineAppConfig } from '#app/nuxt'
export { definePageMeta, PageMeta } from '#app/composables/pages'

After Histoire runs, the same file instead contains:

export { useNuxtApp } from '#build/histoire/composables.mjs'

The definePageMeta declaration is absent. ESLint therefore cannot resolve the auto-imported call's type and reports it as unsafe.

System Info

System:
  OS: macOS 26.5.2
  CPU: (10) arm64 Apple M4
  Memory: 110.45 MB / 24.00 GB
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 24.18.0 - /Users/costleya/.nvm/versions/node/v24.18.0/bin/node
  npm: 11.16.0 - /Users/costleya/.nvm/versions/node/v24.18.0/bin/npm
  pnpm: 10.16.1 - /opt/homebrew/bin/pnpm
  bun: 1.3.14 - /opt/homebrew/bin/bun
Browsers:
  Chrome: 151.0.7922.138
  Safari: 26.5.2
npmPackages:
  @histoire/plugin-nuxt: 1.0.0-beta.1 => 1.0.0-beta.1
  @histoire/plugin-vue: 1.0.0-beta.1 => 1.0.0-beta.1
  histoire: 1.0.0-beta.1 => 1.0.0-beta.1

Used Package Manager

pnpm

Validations

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    to triageThis issue needs to be triaged

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions