rspack-contrib/storybook-rsbuild

Very long compilation time and error "Conflict: Multiple assets emit different content to the same filename iframe.html" when source.entry is set in rsbuild config

Closed this issue · 19 comments

I try to migrate from storybook/vite to storybook with rsbuild but I have a compilation error :

yarn run v1.22.22
$ cross-env NODE_OPTIONS=--max-old-space-size=8192 storybook dev -p 6006
storybook v8.2.2

info => Serving static files from ./.\public at /
info => Starting manager..
info => Starting preview..
info Addon-docs: using MDX3
start   Compiling...
info Using tsconfig paths for react-docgen
error   Compile error: 
Failed to compile, check the errors for troubleshooting.
× Conflict: Multiple assets emit different content to the same filename iframe.html

The error seems non blocking : after closing the overlay, the storybook page run normally

I googled that error but it seems related to webpack, I didn't find any hint

excerp of package.json :

"@rsbuild/core": "1.0.1-beta.1",
    "@rsbuild/plugin-eslint": "1.0.0",
    "@rsbuild/plugin-react": "1.0.1-beta.1",
    "@rsbuild/plugin-type-check": "1.0.1-beta.1",
    "@rsdoctor/rspack-plugin": "^0.3.7",
    "@storybook/addon-essentials": "8.2.2",
    "@storybook/addon-interactions": "8.2.2",
    "@storybook/addon-links": "8.2.2",
    "@storybook/addon-onboarding": "8.2.2",
    "@storybook/blocks": "8.2.2",
    "@storybook/react": "8.2.2",
    "@storybook/react-vite": "8.2.2",
    "@storybook/test": "8.2.2",
    "@storybook/test-runner": "0.19.0",
    "msw-storybook-addon": "2.0.3",
    "playwright": "1.45.1",
    "react-test-renderer": "18.3.1",
    "storybook": "8.2.2",
    "storybook-addon-manual-mocks": "1.0.4",
    "storybook-react-rsbuild": "0.0.7",
    "storybook-addon-remix-react-router": "3.0.0",
    "resolutions": {
       "@storybook/csf": "0.1.11"
    },

storybook config :

import { StorybookConfig } from 'storybook-react-rsbuild'
import { mergeRsbuildConfig } from '@rsbuild/core'

const config: StorybookConfig = {
  stories: [
    "../src/**/*.stories.@(ts|tsx)"
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-onboarding",
    "@storybook/addon-interactions",
    {
      name: "storybook-addon-manual-mocks/vite",
      options: {
        mocksFolder: "mocks",
      }
    },
    'storybook-addon-remix-react-router',
  ],
  framework: {
    name: 'storybook-react-rsbuild',
    options: {}
  },
  rsbuildFinal: (config) => {
    // see https://github.com/rspack-contrib/storybook-rsbuild#customize-the-rsbuild-config
    return mergeRsbuildConfig(config, {
      source: {
        include: [
          /node_modules[\\/]storybook-addon-remix-react-router[\\/]dist[\\/]index.js/
        ]
      }
    })
  },
  staticDirs: ['../public'],
  docs: {
    defaultName: "Docs",
    docsMode: false,
    autodocs: false,
  },
}

export default config

To be honest, I'm not sure without a minimal reproduction. The file conflicted is iframe.html so it's not a hash issue, but way more like multiple pages issue. Could you distinguish which addons / plugins will do the related stuff?

Possible multiple HtmlWebpackPlugin/HtmlRspackPlugin.

You also need to check the rsbuild.config.ts in your project, which will be loaded by storybook-rsbuild by default.

Possible multiple HtmlWebpackPlugin/HtmlRspackPlugin.

No I have none of these plugins installed.

My rsbuild config :

import { defineConfig } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'
import { pluginTypeCheck } from '@rsbuild/plugin-type-check'
import { pluginEslint } from '@rsbuild/plugin-eslint'

const shouldUseRsdoctor = process.env.RSDOCTOR === 'true'

export default defineConfig({
  server: {
    port: 3001,
  },
  html: {
    title: "Application paie",
  },
  source: {
    entry: {
      index: './src/index.tsx',
    }
  },
  output: {
    distPath: {
      root: 'build'
    },
    cleanDistPath: true
  },
  plugins: [
    /** désactivé car prend trop de mémoire, souvent OOM */
    pluginTypeCheck({ enable: false }),
    /** désactivé car prend presque 30s lors du build */
    pluginEslint({ enable: false }),
    pluginReact()
  ],
  tools: {
    rspack(config, { appendPlugins }) {
      // Only register the plugin when RSDOCTOR is true, as the plugin will increase the build time.
      if (shouldUseRsdoctor) {
        appendPlugins(
          new RsdoctorRspackPlugin({
            linter: {
              //  When set to `'Error'`, all rules will be executed
              level: 'Error',
              extends: [],
              rules: {
                'default-import-check': 'on',
                'duplicate-package': 'on',
                'loader-performance-optimization': 'on',
                'ecma-version-check': "off",
              },
            },
          })
        )
      }
      config.experiments = {
        ...(config.experiments ?? {}),
        // https://www.rspack.dev/config/experiments#experimentslazycompilation
        // désactivé car ne semble pas fonctionner correctement
        lazyCompilation: false
      }
      return config
    }
  }
})

The config you provided seems common, I can't tell what's going wrong. Could you provide a minimal reproduction?

I was able to reproduce the issue and create a codesandbox here: https://codesandbox.io/p/devbox/jolly-water-lhl99t

It seems that the error is adding this to the rsbuild.config.ts:

  source: {
    entry: {
      index: "./src/index.tsx",
    },
  },

Removing entry resolves the Storybook build issue and doesn't seem to affect my Rsbuild — both dev and build seem to work fine, even in a much larger project.

@phildenhoff Nice catch, that's the issue where it raised. As per https://rsbuild.dev/config/source/entry, Rsbuild will register html-rspack-plugin for each entry, no matter it's explicit or implicit.
So, when not explicitly adding an entry (using default entry then), no extra entry in config file will be loaded by Storybook builder, everything goes well. When adding one, multiple entries' collision happens, and they're trying to emit same output file.
The workaround is to comply and use the default entry of Rsbuild, and remove the explicit added one. I'll take a look into other builders' implementation and see how they deal with the multiple entries. (filter out or de-confilict).

Removing entry resolves the Storybook build issue and doesn't seem to affect my Rsbuild — both dev and build seem to work fine, even in a much larger project.

thx @phildenhoff I was suspecting something like that , actually I did try to set entry: null in the rsbuild override in storybook/main, but with no luck

thx @phildenhoff I was suspecting something like that , actually I did try to set entry: null in the rsbuild override in storybook/main, but with no luck

Oh I got it, it didn't work because I've used mergeRsbuildConfig which merge the configs, so the entry property was retained.
If I do the following change in storybook/main, it works now :

rsbuildFinal: (config) => {
    const cfg = mergeRsbuildConfig(config, {
      dev: {
        client: {
          overlay: false
        }
      },
      source: {
        include: [
          /node_modules[\\/]storybook-addon-remix-react-router[\\/]dist[\\/]index.js/
        ]
      }
    })
	// here we remove the entry property
    delete cfg.environments?.web?.source?.entry
    return cfg
  },

but as @phildenhoff said, it also works by removing entry property in the origignal rsbuild config.

I'm not seeing the usage merging project's rsbuild.config.entry into Storybook. Will stop source.entry from being merged.

I've tried 0.0.9 the last version and removed my fix in main.ts, but weirdly the pb is still there.
I've had to add back my fix.

environments.x.source.entry need to be removed alongside, I'll check it again.

@abenhamdine Are you using https://rsbuild.dev/zh/config/environments in your project's rsbuild.config?

@abenhamdine Are you using rsbuild.dev/zh/config/environments in your project's rsbuild.config?

Ah yes, indeed :

environments: {
    web: {
      source: {
        entry: {
          index: './src/index.tsx',
        },
      },
...

That's interesting. When starting Storybook with environments config, what's the expected behavior here?

Storybook loads the config file in project by default, to make components in the iframe share config of the project as much as possible, and only tweak some config to make sure it could run inside Storybook iframe container.

So there might be some scenarios:

  • no environments: merge project config, strip some fields (e.g. entry) that should be totally overridden by Storybook (current implementation).
  • single environment: same as no environments.
  • multiple environments: Storybook might pick at least one environment to merge with. In an extreme scenario, the web and node env only shares plugins config, then Storybook should pick web env to inherit, but how could the builder know that, not to mention, it might use my-web or any random name.

Considering this, I think we need to add an option to the builder config, type defined as:

{ environments?: string | string[] }

The environments option defaults to undefined, heuristic. As described above:

  1. directly merge when without environments.
  2. behaves as 1. when there's a single environment.
  3. when multiple environments occurred, throw error until user pick which environments to inherit (entry should be striped for all picked environments).

@chenjiahan @9aoy What do you think, any guidance of the usage of environments here?

9aoy commented

@fi3ework I'm not sure if there are scenarios in Storybook that require building for multiple environments, and whether all environments should apply the default Storybook configuration.

I think it would be practical to have a default environment in Storybook(perhaps named 'storybook' ?), and if the user needs to add a new build scenario, they can choose to add a new environment configuration.

@9aoy

I'm not sure if there are scenarios in Storybook that require building for multiple environments

I'm not seeing that, I think the answer is no for now.

I think it would be practical to have a default environment in Storybook(perhaps named 'storybook' ?)

It does have some config, but it needs to find a user's base config and extends it. The dilemma here is environments brings more than one usable config, and can't figure out which one to use when multiple environements exist.

Storybook can read the web environment by default (if it is configured), and allow users to configure reading of another environment if required.

it works now with version 0.0.10 and web environment, thx a lot 👍