Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular: Remove cached NgModules and introduce a global queue during bootstrapping #24982

Merged

Conversation

Marklb
Copy link
Member

@Marklb Marklb commented Nov 25, 2023

Closes #24813

What I did

Problem: Caching the NgModule per component, causes the NgModule generated for the first story to get used for the following stories, but they may not have generated the same NgModule.

Explanation:

Assume the following is the CSF file:

export default {
  component: FooComponent,
}

export const Primary = { }
export const Secondary = {
  moduleMetadata: {
    declarations: [ BarComponent ],
  },
}

In the renderer, the following is basically what the NgModule would be for each story:

// Primary
@NgModule({
  declarations: [ FooComponent ],
})
class StorybookModule {}

// Secondary
@NgModule({
  declarations: [ FooComponent, BarComponent ],
})
class StorybookModule {}

When using inline rendering to render both stories in a single document, Storybook attempts to render each story, without waiting on the previous to complete. The problem is that Angular only allows a component to be declared by a single NgModule, so we have to clear the compiled component cache after each story's app is initialized.

The solution being used currently, would cache the NgModule by the component defined in Meta. That avoids the error, but breaks the "Secondary" story, because BarComponent is not declared in the NgModule that was cached when the "Primary" story rendered (Assuming the "Primary" story rendered first.).

I don't know if the way inline stories render changed and used to wait for completion of each story or if it just wasn't fully implemented as expected. AbstractRenderer already has a method that calls ɵresetCompiledComponents and both CanvasRenderer and DocsRenderer call it after a full render. The problem is two render methods can be running in parallel, which makes the reset not work correctly.

To fix ɵresetCompiledComponents not getting called correctly, a queue is introduced to the AbstractRenderer that prevents calling bootstrapApplication in parallel. This may have an effect on performance, if a particular story is slow to initialize, but I don't know of another way to avoid conflicts between each Angular application's compiling.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

To see the problem with caching the NgModule,

  1. Run a sandbox for template, e.g. yarn task --task sandbox --start-from auto --template angular-cli/default-ts
  2. In the sandbox file stories/core/decorators/componentWrapperDecorator/decorators.stories.ts, add tags: ['autodocs'] to the default export Meta.
  3. Open Storybook in your browser
  4. Access "frameworks > angular > core > decorators > componentWrapperDecorator > decorators > Docs" stories. Before this change, all the stories render something, but any of the stories declaring ParentComponent threw an error, because the first story did not declare ParentComponent. With this change all should render correctly.

To see the reason for adding the lock, instead of just removing the NgModule caching, you can do the same steps, but comment out await acquireLock(selector); in the the bootstrapLock function in code/frameworks/angular/src/client/angular-beta/utils/BootstrapLock.ts. Commenting that line would disable the lock and cause the multiple modules declaring component error that the caching was supposed to fix.

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=<PR_NUMBER>

@Marklb Marklb marked this pull request as ready for review November 30, 2023 04:47
@Marklb Marklb changed the title [Angular] Remove cached NgModules and introduce a global lock during bootstrapping Angular: Remove cached NgModules and introduce a global lock during bootstrapping Nov 30, 2023
@valentinpalkovic valentinpalkovic removed the request for review from yannbf December 1, 2023 07:22
@valentinpalkovic
Copy link
Contributor

valentinpalkovic commented Dec 1, 2023

@Marklb Thank you so much for figuring out the root cause of the issue. This was the missing peace. I am so grateful that you dived deep into it!

I have thought about your solution, which works great. The only thing might be that renderings might happen in no particular order. I have pushed a different approach using queues. WDYT? Then we don't have to rely on setTimeout, but the new solution is more promise-based.

@Marklb
Copy link
Member Author

Marklb commented Dec 1, 2023

@valentinpalkovic It is late for me, so I can't try it right now, but I looked at the code from my phone and I do like not having the setTimeout and more predictable order.

@valentinpalkovic
Copy link
Contributor

No pressure! I will wait for your proper feedback. I also changed when the compiled modules got reset in the latest commit because otherwise, your newly written test case of mounting two angular components at once would fail.

@Marklb
Copy link
Member Author

Marklb commented Dec 2, 2023

I updated a test to be more thorough, adjusted the names and wording from "lock" to "queue", and added back the comment explaining why the queue is necessary.

Other than getting your opinion on the questions I added about the resetCompiledComponents move, the changes look good to me.

@Marklb Marklb changed the title Angular: Remove cached NgModules and introduce a global lock during bootstrapping Angular: Remove cached NgModules and introduce a global queue during bootstrapping Dec 2, 2023
@valentinpalkovic
Copy link
Contributor

Hi @Marklb

Could you try to resolve the conflicts? Then, I will give CI another try. If it still fails, I will take a look!

@Marklb
Copy link
Member Author

Marklb commented Dec 12, 2023

@valentinpalkovic The conflicts should be fixed now.

@valentinpalkovic
Copy link
Contributor

Great! I will take a look tomorrow at why CI is failing

@Marklb
Copy link
Member Author

Marklb commented Dec 28, 2023

I tried to update the tests for vitest, but I am not familiar with vitest, yet. The BootstrapQueue tests sometimes pass and sometimes fail. After debugging, it seems that the tests are running in parallel, without being isolated. I am not positive that is what happens, but stepping through breakpoints, once a test does something async the next test starts then the next async action will go back to a breakpoint in the first test.

I am just assuming that is the problem and maybe I need to add something to stop that or that isn't even the problem, but I will look into it later, when I have more time to dig into it.

@valentinpalkovic
Copy link
Contributor

Thanks for taking care of it! I try to support you as soon as I have some free resources.

But maybe, first of all, it would make sense to understand why the UI Test in Chromatic fails.

Regarding the unit test: Can we run the whole test suite and its tasks sequentially? Maybe Vitest offers some options for doing so exclusively for a particular test suite.

@valentinpalkovic valentinpalkovic merged commit 9f52ab4 into storybookjs:next Jan 17, 2024
47 of 48 checks passed
@github-actions github-actions bot mentioned this pull request Jan 17, 2024
21 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: Missing ng* parameters for elements injected with ng-content (Angular)
2 participants