storybook icon indicating copy to clipboard operation
storybook copied to clipboard

[Bug]: NgModule is imported twice

Open nzacca opened this issue 1 year ago • 2 comments

Describe the bug

When an NgModule follows the forRoot pattern (https://angular.io/guide/singleton-services#prevent-reimport-of-the-greetingmodule), the module appears to be loaded twice and produces a runtime error indicating that the module was already loaded.

TestModule is already loaded. Import it in the AppModule only
Error: TestModule is already loaded. Import it in the AppModule only
    at new TestModule (http://localhost:6006/app-test-stories.iframe.bundle.js:61:13)
    at Object.TestModule_Factory [as useFactory] (ng:///TestModule/ɵfac.js:4:10)
    at Object.factory (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:43968:32)
    at R3Injector.hydrate (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:43886:29)
    at R3Injector.get (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:43787:23)
    at injectInjectorOnly (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:36619:29)
    at ɵɵinject (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:36623:59)
    at useValue (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:43585:25)
    at R3Injector.resolveInjectorInitializers (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:43827:9)
    at new EnvironmentNgModuleRefAdapter (http://localhost:6006/vendors-node_modules_storybook_addon-docs_angular_index_js-node_modules_storybook_addon-essen-170b5a.iframe.bundle.js:55368:14)

Note this pattern works fine in Angular 15 environment.

To Reproduce

// test.module.ts
import {
  ModuleWithProviders,
  NgModule,
  Optional,
  SkipSelf,
} from '@angular/core';

@NgModule()
export class TestModule {
  constructor(@Optional() @SkipSelf() parentModule?: TestModule) {
    if (parentModule) {
      throw new Error(
        'TestModule is already loaded. Import it in the AppModule only'
      );
    }
  }

  static forRoot(): ModuleWithProviders<TestModule> {
    return {
      ngModule: TestModule,
      providers: [],
    };
  }
}

// test.stories.ts
import { Component } from '@angular/core';
import { moduleMetadata, type Meta, type StoryFn } from '@storybook/angular';
import { TestModule } from './test.module';

@Component({
  selector: 'app-test',
  template: ``,
})
class TestComponent {}

export default {
  title: 'Module Test',
  component: TestComponent,
  decorators: [moduleMetadata({ imports: [TestModule.forRoot()] })],
} as Meta;

const Template: StoryFn<TestComponent> = (args: TestComponent) => ({
  props: args,
});

export const Test = Template.bind({});

System

Environment Info:

  System:
    OS: Windows 10 10.0.22621
    CPU: (24) x64 AMD Ryzen 9 3900X 12-Core Processor
  Binaries:
    Node: 18.14.2 - C:\dev\nodejs\node.EXE
    npm: 9.5.0 - C:\dev\nodejs\npm.CMD
  Browsers:
    Chrome: 110.0.5481.104
    Edge: Spartan (44.22621.1265.0), Chromium (110.0.1587.57)
  npmPackages:
    @storybook/addon-actions: 7.0.0-beta.55 => 7.0.0-beta.55
    @storybook/addon-essentials: 7.0.0-beta.55 => 7.0.0-beta.55
    @storybook/addon-interactions: 7.0.0-beta.55 => 7.0.0-beta.55
    @storybook/addon-links: 7.0.0-beta.55 => 7.0.0-beta.55
    @storybook/angular: 7.0.0-beta.55 => 7.0.0-beta.55
    @storybook/builder-webpack5: 7.0.0-beta.55 => 7.0.0-beta.55
    @storybook/testing-library: ^0.0.13 => 0.0.13

Additional context

Is it possible that the @SkipSelf is not being respected on the Module constructor parameter? Possibly related to this? https://github.com/storybookjs/storybook/issues/21243

Is it appropriate to import forRoot modules in the Story metadata? Do we need a "special" property for root imports perhaps?

nzacca avatar Feb 28 '23 05:02 nzacca

@sheriffMoose @valentinpalkovic is this something either of you know about?

shilman avatar Mar 17 '23 14:03 shilman

Yes, working on it already.

valentinpalkovic avatar Mar 17 '23 14:03 valentinpalkovic

It doesn't happen only for a Module.forRoot(), it also happens for modules that are needed to be imported only once in the application like BrowserModule and BrowserAnimationsModule. Once you create a story that imports these modules (especially since BrowserAnimationsModule is needed) you get a runtime error: Providers from the `BrowserModule` have already been loaded

Hey @valentinpalkovic, I would be happy to know of a timeline for a fix. I upgraded to sb7 and my components won't work. I just want to know if I can wait for it or I need to downgrade to sb6.

Thanks!

rezoled avatar Mar 21 '23 22:03 rezoled

@rezoled the error should not occur for BrowserAnimationModule, because we handle this separately. Please provide a reproduction in a separate issue, because this is unrelated to this one.

valentinpalkovic avatar Mar 22 '23 06:03 valentinpalkovic

The timeline is to work on this bug this or next week.

valentinpalkovic avatar Mar 22 '23 06:03 valentinpalkovic

What about this error, is it the same bug? NG04007: The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead

rezoled avatar Mar 22 '23 08:03 rezoled

That's related to the same error mentioned in this issue

valentinpalkovic avatar Mar 22 '23 08:03 valentinpalkovic

I think I understand the issue with BrowserAnimationsModule, when I import it like this: imports: [BrowserAnimationsModule] I get not errors, however when I specify configurations:

imports[
	BrowserAnimationsModule.withConfig({
		disableAnimations: isDev,
	}),
]

It throws Providers from the `BrowserModule` have already been loaded

rezoled avatar Mar 22 '23 09:03 rezoled

Another error I get is:

Type AccountNameComponent is part of the declarations of 2 modules: AccountNameModule and AccountNameModule! Please consider moving AccountNameComponent to a higher module that imports AccountNameModule and AccountNameModule. You can also create a new NgModule that exports and includes AccountNameComponent then import that NgModule in AccountNameModule and AccountNameModule

It seems that Angular tries to generate the same module twice for the same context.

rezoled avatar Mar 22 '23 09:03 rezoled

@rezoled

You can fix the browser animation error by applying the following code:

{
  ...
  imports:[isDev ? NoopAnimationsModule : BrowserAnimationsModule]
}

valentinpalkovic avatar Mar 22 '23 15:03 valentinpalkovic

Another error I get is:

Type AccountNameComponent is part of the declarations of 2 modules: AccountNameModule and AccountNameModule! Please consider moving AccountNameComponent to a higher module that imports AccountNameModule and AccountNameModule. You can also create a new NgModule that exports and includes AccountNameComponent then import that NgModule in AccountNameModule and AccountNameModule

It seems that Angular tries to generate the same module twice for the same context.

@rezoled This is another error and does not relate to this Github Issue. Please open a new issue with a reproduction.

valentinpalkovic avatar Mar 22 '23 15:03 valentinpalkovic

Great Caesar's ghost!! I just released https://github.com/storybookjs/storybook/releases/tag/v7.0.0-rc.11 containing PR #21770 that references this issue. Upgrade today to the @next NPM tag to try it out!

npx sb@next upgrade --prerelease

shilman avatar Mar 31 '23 10:03 shilman

@shilman we have upgraded to RC11 and we still see this error:

core.mjs:8405 ERROR Error: NG04007: The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead. at Object.provideForRootGuard [as useFactory] (router.mjs:7009:1) at Object.factory (core.mjs:8107:1) at R3Injector.hydrate (core.mjs:8020:1) at R3Injector.get (core.mjs:7908:1) at injectInjectorOnly (core.mjs:633:1) at Module.ɵɵinject (core.mjs:637:1) at Object.RouterModule_Factory [as useFactory] (router.mjs:6962:1) at Object.factory (core.mjs:8107:1) at R3Injector.hydrate (core.mjs:8020:1) at R3Injector.get (core.mjs:7908:1)

This is migrating an existing moduleMetadata that used to work in version 6.

jinder avatar Mar 31 '23 13:03 jinder

@jinder Please follow the migration guide: https://github.com/storybookjs/storybook/blob/v7.0.0-rc.11/MIGRATION.md#angular-application-providers-and-modulewithproviders

valentinpalkovic avatar Mar 31 '23 13:03 valentinpalkovic

@valentinpalkovic thanks - this now works! We were getting some strange errors because a child module was importing BrowserAnimationsModule directly.

jinder avatar Mar 31 '23 13:03 jinder

@jinder i think BrowserAnimationsModule should always be imported on the root level and should never be imported by a child module if I am not wrong. Please open a new issue, if you still have problems

valentinpalkovic avatar Mar 31 '23 13:03 valentinpalkovic

@valentinpalkovic I think you're right, however Angular doesn't error or warn you. It's only apparent when using Storybook and the errors were a bit confusing. It all works now though, thank you!

jinder avatar Mar 31 '23 13:03 jinder

I'm still getting duplicate module error:

Type AvatarComponent is part of the declarations of 2 modules: AvatarModule and StorybookComponentModule! Please consider moving AvatarComponent to a higher module that imports AvatarModule and StorybookComponentModule. You can also create a new NgModule that exports and includes AvatarComponent then import that NgModule in AvatarModule and StorybookComponentModule.

The test:

export default {
	component: AvatarComponent,
	decorators: [
		applicationConfig({
			providers: [importProvidersFrom(AvatarModule)],
		}),
	],
} as Meta;

avatar.module:

import { NgModule } from '@angular/core';
import { AvatarComponent } from './avatar.component';

@NgModule({
	declarations: [AvatarComponent],
	exports: [AvatarComponent],
})
export class AvatarModule {}

I tried running ``npx sb@next upgrade --prerelease``` but I got error while running migrations. However I did update the pacakges:

		"@storybook/addon-essentials": "7.0.0",
		"@storybook/core-server": "7.0.0",
		"@storybook/angular": "7.0.0",

Is it something I'm doing wrong? Could it be because of the migration error?

rezoled avatar Apr 02 '23 10:04 rezoled

@rezoled please open a new issue and mention me, because this issue was about the forRoot pattern (and if possible, please provide a reproduction). I have already a solution in mind for your problem. I will take a look on Monday.

valentinpalkovic avatar Apr 02 '23 11:04 valentinpalkovic

No problem, opening a new issue. Meanwhile I checked and the problem was not from the broken migration

rezoled avatar Apr 02 '23 11:04 rezoled