diff --git a/.circleci/config.yml b/.circleci/config.yml index 04f5390f0cda..72723c16f307 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -599,23 +599,23 @@ workflows: requires: - build - create-sandboxes: - parallelism: 16 + parallelism: 15 requires: - build - build-sandboxes: - parallelism: 16 + parallelism: 15 requires: - create-sandboxes - test-runner-sandboxes: - parallelism: 16 + parallelism: 15 requires: - build-sandboxes - chromatic-sandboxes: - parallelism: 16 + parallelism: 15 requires: - build-sandboxes - e2e-sandboxes: - parallelism: 16 + parallelism: 15 requires: - build-sandboxes daily: @@ -624,25 +624,25 @@ workflows: jobs: - build - create-sandboxes: - parallelism: 28 + parallelism: 27 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 28 + parallelism: 27 requires: - create-sandboxes - test-runner-sandboxes: - parallelism: 28 + parallelism: 27 requires: - build-sandboxes - chromatic-sandboxes: - parallelism: 28 + parallelism: 27 requires: - build-sandboxes - e2e-sandboxes: - parallelism: 28 + parallelism: 27 requires: - build-sandboxes diff --git a/MIGRATION.md b/MIGRATION.md index f50dae7b2eec..7ef2a90fc94b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -40,6 +40,9 @@ - [Stories glob matches MDX files](#stories-glob-matches-mdx-files) - [Add strict mode](#add-strict-mode) - [Removed DLL flags](#removed-dll-flags) + - [Angular: Drop support for Angular \< 14](#angular-drop-support-for-angular--14) + - [Angular: Drop support for calling Storybook directly](#angular-drop-support-for-calling-storybook-directly) + - [Angular: Removed legacy renderer](#angular-removed-legacy-renderer) - [Docs Changes](#docs-changes) - [Standalone docs files](#standalone-docs-files) - [Referencing stories in docs files](#referencing-stories-in-docs-files) @@ -765,6 +768,18 @@ If user code in `.storybook/preview.js` or stories relies on "sloppy" mode behav Earlier versions of Storybook used Webpack DLLs as a performance crutch. In 6.1, we've removed Storybook's built-in DLLs and have deprecated the command-line parameters `--no-dll` and `--ui-dll`. In 7.0 those options are removed. +#### Angular: Drop support for Angular < 14 + +Starting in 7.0, we drop support for Angular < 14 + +#### Angular: Drop support for calling Storybook directly + +In Storybook 6.4 we have deprecated calling Storybook directly (`npm run storybook`) for Angular. In Storybook 7.0, we've removed it entirely. Instead you have to set up the Storybook builder in your `angular.json` and execute `ng run :storybook` to start Storybook. Please visit https://github.com/storybookjs/storybook/tree/next/code/frameworks/angular to set up Storybook for Angular correctly. + +#### Angular: Removed legacy renderer + +The `parameters.angularLegacyRendering` option is removed. You cannot use the old legacy renderer anymore. + ### Docs Changes The information hierarchy of docs in Storybook has changed in 7.0. The main difference is that each docs is listed in the sidebar as a separate entry, rather than attached to individual stories. diff --git a/code/addons/docs/angular/README.md b/code/addons/docs/angular/README.md index 2e1bf1ab2291..92d9867076fe 100644 --- a/code/addons/docs/angular/README.md +++ b/code/addons/docs/angular/README.md @@ -13,6 +13,8 @@ To learn more about Storybook Docs, read the [general documentation](../README.m - [Installation](#installation) - [DocsPage](#docspage) - [Props tables](#props-tables) + - [Automatic Compodoc setup](#automatic-compodoc-setup) +- [Manual Compodoc setup](#manual-compodoc-setup) - [MDX](#mdx) - [IFrame height](#iframe-height) - [Inline Stories](#inline-stories) @@ -42,35 +44,63 @@ When you [install docs](#installation) you should get basic [DocsPage](../docs/d Getting [Props tables](../docs/props-tables.md) for your components requires a few more steps. Docs for Angular relies on [Compodoc](https://compodoc.app/), the excellent API documentation tool. It supports `inputs`, `outputs`, `properties`, `methods`, `view/content child/children` as first class prop types. -To get this, you'll first need to install Compodoc: +### Automatic Compodoc setup + +During `sb init`, you will be asked, whether you want to setup Compodoc for your project. Just answer the question with Yes. Compodoc is then ready to use! + +## Manual Compodoc setup + +You'll need to register Compodoc's `documentation.json` file in `.storybook/preview.ts`: + +```js +import { setCompodocJson } from '@storybook/addon-docs/angular'; +import docJson from '../documentation.json'; + +setCompodocJson(docJson); +``` + +Finally, to set up compodoc, you'll first need to install Compodoc: ```sh yarn add -D @compodoc/compodoc ``` -Then you'll need to configure Compodoc to generate a `documentation.json` file. Adding the following snippet to your `package.json` creates a metadata file `./documentation.json` each time you run storybook: +Then you'll need to configure Compodoc to generate a `documentation.json` file. Adding the following snippet to your `projects..architect.` in the `angular.json` creates a metadata file `./documentation.json` each time you run storybook: -```json +```jsonc +// angular.json { - ... - "scripts": { - "docs:json": "compodoc -p ./tsconfig.json -e json -d .", - "storybook": "npm run docs:json && start-storybook -p 6006 -s src/assets", - ... - }, + "projects": { + "your-project": { + "architect": { + "storybook": { + ..., + "compodoc": true, + "compodocArgs": [ + "-e", + "json", + "-d", + "." // the root folder of your project + ], + }, + "build-storybook": { + ..., + "compodoc": true, + "compodocArgs": [ + "-e", + "json", + "-d", + "." // the root folder of your project + ], + } + } + } + } } ``` Unfortunately, it's not currently possible to update this dynamically as you edit your components, but [there's an open issue](https://github.com/storybookjs/storybook/issues/8672) to support this with improvements to Compodoc. -Next, add the following to `.storybook/preview.ts` to load the Compodoc-generated file: - -```js -import { setCompodocJson } from '@storybook/addon-docs/angular'; -import docJson from '../documentation.json'; -setCompodocJson(docJson); -``` - Finally, be sure to fill in the `component` field in your story metadata: ```ts diff --git a/code/addons/storyshots-core/package.json b/code/addons/storyshots-core/package.json index 738a35231539..501fde008571 100644 --- a/code/addons/storyshots-core/package.json +++ b/code/addons/storyshots-core/package.json @@ -67,7 +67,7 @@ "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", "enzyme-to-json": "^3.6.1", - "jest-preset-angular": "^8.3.2", + "jest-preset-angular": "^12.2.3", "jest-vue-preprocessor": "^1.7.1", "react-test-renderer": "^16", "rimraf": "^3.0.2", @@ -82,7 +82,7 @@ "@storybook/vue": "*", "@storybook/vue3": "*", "jest": "*", - "jest-preset-angular": "*", + "jest-preset-angular": " >= 12.2.3", "jest-vue-preprocessor": "*", "preact": "^10.5.13", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", diff --git a/code/addons/storyshots-core/src/frameworks/angular/loader.ts b/code/addons/storyshots-core/src/frameworks/angular/loader.ts index 7b2406c3c92e..f472ff8f9ada 100644 --- a/code/addons/storyshots-core/src/frameworks/angular/loader.ts +++ b/code/addons/storyshots-core/src/frameworks/angular/loader.ts @@ -17,11 +17,7 @@ function setupAngularJestPreset() { // is running inside jest - one of the things that `jest-preset-angular/build/setupJest` does is // extending the `window.Reflect` with all the needed metadata functions, that are required // for emission of the TS decorations like 'design:paramtypes' - try { - jest.requireActual('jest-preset-angular/build/setupJest'); - } catch (e) { - jest.requireActual('jest-preset-angular/build/setup-jest'); - } + jest.requireActual('jest-preset-angular/setup-jest'); } function test(options: StoryshotsOptions): boolean { diff --git a/code/addons/storyshots-core/src/frameworks/angular/renderTree.ts b/code/addons/storyshots-core/src/frameworks/angular/renderTree.ts index 5385f4a85d7b..9de180774184 100644 --- a/code/addons/storyshots-core/src/frameworks/angular/renderTree.ts +++ b/code/addons/storyshots-core/src/frameworks/angular/renderTree.ts @@ -1,9 +1,8 @@ -import AngularSnapshotSerializer from 'jest-preset-angular/build/AngularSnapshotSerializer'; -import HTMLCommentSerializer from 'jest-preset-angular/build/HTMLCommentSerializer'; +import AngularSnapshotSerializer from 'jest-preset-angular/build/serializers/ng-snapshot'; +import HTMLCommentSerializer from 'jest-preset-angular/build/serializers/html-comment'; import { TestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { addSerializer } from 'jest-specific-snapshot'; -import { getStorybookModuleMetadata } from '@storybook/angular/renderer'; +import { getApplication, storyPropsProvider } from '@storybook/angular/renderer'; import { BehaviorSubject } from 'rxjs'; addSerializer(HTMLCommentSerializer); @@ -12,31 +11,20 @@ addSerializer(AngularSnapshotSerializer); function getRenderedTree(story: any) { const currentStory = story.render(); - const moduleMeta = getStorybookModuleMetadata( - { - storyFnAngular: currentStory, - component: story.component, - // TODO : To change with the story Id in v7. Currently keep with static id to avoid changes in snapshots - targetSelector: 'storybook-wrapper', - }, - new BehaviorSubject(currentStory.props) - ); - - TestBed.configureTestingModule({ - imports: [...moduleMeta.imports], - declarations: [...moduleMeta.declarations], - providers: [...moduleMeta.providers], - schemas: [...moduleMeta.schemas], + const application = getApplication({ + storyFnAngular: currentStory, + component: story.component, + // TODO : To change with the story Id in v7. Currently keep with static id to avoid changes in snapshots + targetSelector: 'storybook-wrapper', }); - TestBed.overrideModule(BrowserDynamicTestingModule, { - set: { - entryComponents: [...moduleMeta.entryComponents], - }, + TestBed.configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(new BehaviorSubject(currentStory.props))], }); return TestBed.compileComponents().then(() => { - const tree = TestBed.createComponent(moduleMeta.bootstrap[0] as any); + const tree = TestBed.createComponent(application); tree.detectChanges(); // Empty componentInstance remove attributes of the internal main component () in snapshot diff --git a/code/frameworks/angular/README.md b/code/frameworks/angular/README.md index ceb998025f85..8ef91c279af6 100644 --- a/code/frameworks/angular/README.md +++ b/code/frameworks/angular/README.md @@ -1,5 +1,11 @@ # Storybook for Angular +- [Storybook for Angular](#storybook-for-angular) + - [Getting Started](#getting-started) + - [Setup Compodoc](#setup-compodoc) + - [Support for multi-project workspace](#support-for-multi-project-workspace) + - [Run Storybook](#run-storybook) + Storybook for Angular is a UI development environment for your Angular components. With it, you can visualize different states of your UI components and develop them interactively. @@ -15,6 +21,66 @@ cd my-angular-app npx storybook init ``` +### Setup Compodoc + +When installing, you will be given the option to set up Compodoc, which is a tool for creating documentation for Angular projects. + +You can include JSDoc comments above components, directives, and other parts of your Angular code to include documentation for those elements. Compodoc uses these comments to generate documentation for your application. In Storybook, it is useful to add explanatory comments above @Inputs and @Outputs, since these are the main elements that Storybook displays in its user interface. The @Inputs and @Outputs are the elements that you can interact with in Storybook, such as controls. + +## Support for multi-project workspace + +Storybook supports Angular multi-project workspace. You can setup Storybook for each project in the workspace. When running `npx storybook init` you will be asked for which project Storybook should be set up. Essentially, during initialization, the `angular.json` will be edited to add the Storybook configuration for the selected project. The configuration looks approximately like this: + +```json +// angular.json +{ + ... + "projects": { + ... + "your-project": { + ... + "architect": { + ... + "storybook": { + "builder": "@storybook/angular:start-storybook", + "options": { + "configDir": ".storybook", + "browserTarget": "your-project:build", + "compodoc": false, + "port": 6006 + } + }, + "build-storybook": { + "builder": "@storybook/angular:build-storybook", + "options": { + "configDir": ".storybook", + "browserTarget": "your-project:build", + "compodoc": false, + "outputDir": "dist/storybook/your-project" + } + } + } + } + } +} +``` + +## Run Storybook + +To run Storybook for a particular project, please run: + +```sh +ng run your-project:storybook +``` + +To build Storybook, run: + +```sh +ng run your-project:build-storybook +``` + +You will find the output in `dist/storybook/your-project`. + For more information visit: [storybook.js.org](https://storybook.js.org) --- diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 0e5dc59dd7e6..cdcad4f7d6ff 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@storybook/builder-webpack5": "7.0.0-beta.29", + "@storybook/cli": "7.0.0-beta.29", "@storybook/client-logger": "7.0.0-beta.29", "@storybook/core-client": "7.0.0-beta.29", "@storybook/core-common": "7.0.0-beta.29", @@ -65,23 +66,22 @@ "webpack": "5" }, "devDependencies": { - "@angular-devkit/architect": "^0.1303.5", - "@angular-devkit/build-angular": "^13.3.5", - "@angular-devkit/core": "^13.3.5", - "@angular/cli": "^13.3.5", - "@angular/common": "^13.3.6", - "@angular/compiler": "^13.3.6", - "@angular/compiler-cli": "^13.3.6", - "@angular/core": "^13.3.6", - "@angular/forms": "^13.3.6", - "@angular/platform-browser": "^13.3.6", - "@angular/platform-browser-dynamic": "^13.3.6", - "@nrwl/workspace": "14.6.1", + "@angular-devkit/architect": "^0.1500.4", + "@angular-devkit/build-angular": "^15.0.4", + "@angular-devkit/core": "^15.0.4", + "@angular/cli": "^15.0.4", + "@angular/common": "^15.0.4", + "@angular/compiler": "^15.0.4", + "@angular/compiler-cli": "^15.0.4", + "@angular/core": "^15.0.4", + "@angular/forms": "^15.0.4", + "@angular/platform-browser": "^15.0.4", + "@angular/platform-browser-dynamic": "^15.0.4", "@types/rimraf": "^3.0.2", "@types/tmp": "^0.2.3", "cross-spawn": "^7.0.3", "jest": "^29.3.1", - "jest-preset-angular": "^12.0.0", + "jest-preset-angular": "^12.2.3", "jest-specific-snapshot": "^7.0.0", "rimraf": "^3.0.2", "tmp": "^0.2.1", @@ -90,19 +90,18 @@ "zone.js": "^0.12.0" }, "peerDependencies": { - "@angular-devkit/architect": ">=0.1300.0", - "@angular-devkit/build-angular": ">=13.0.0", - "@angular-devkit/core": ">=13.0.0", - "@angular/cli": ">=13.0.0", - "@angular/common": ">=13.0.0", - "@angular/compiler": ">=13.0.0", - "@angular/compiler-cli": ">=13.0.0", - "@angular/core": ">=13.0.0", - "@angular/forms": ">=13.0.0", - "@angular/platform-browser": ">=13.0.0", - "@angular/platform-browser-dynamic": ">=13.0.0", + "@angular-devkit/architect": ">=0.1400.0", + "@angular-devkit/build-angular": ">=14.0.0", + "@angular-devkit/core": ">=14.0.0", + "@angular/cli": ">=14.0.0", + "@angular/common": ">=14.0.0", + "@angular/compiler": ">=14.0.0", + "@angular/compiler-cli": ">=14.0.0", + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@angular/platform-browser": ">=14.0.0", + "@angular/platform-browser-dynamic": ">=14.0.0", "@babel/core": "*", - "@nrwl/workspace": "14.6.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "rxjs": "^6.0.0 || ^7.4.0", @@ -112,9 +111,6 @@ "peerDependenciesMeta": { "@angular/cli": { "optional": true - }, - "@nrwl/workspace": { - "optional": true } }, "engines": { diff --git a/code/frameworks/angular/src/builders/build-storybook/index.ts b/code/frameworks/angular/src/builders/build-storybook/index.ts index 1698051fd23a..5d10ddfbd03b 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.ts @@ -11,13 +11,10 @@ import { CLIOptions } from '@storybook/types'; import { catchError, map, mapTo, switchMap } from 'rxjs/operators'; import { sync as findUpSync } from 'find-up'; import { sync as readUpSync } from 'read-pkg-up'; -import { - BrowserBuilderOptions, - ExtraEntryPoint, - StylePreprocessorOptions, -} from '@angular-devkit/build-angular'; +import { BrowserBuilderOptions, StylePreprocessorOptions } from '@angular-devkit/build-angular'; -import { buildStaticStandalone } from '@storybook/core-server'; +import { buildStaticStandalone, withTelemetry } from '@storybook/core-server'; +import { StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; import { StandaloneOptions } from '../utils/standalone-options'; import { runCompodoc } from '../utils/run-compodoc'; import { buildStandaloneErrorHandler } from '../utils/build-standalone-errors-handler'; @@ -28,16 +25,18 @@ export type StorybookBuilderOptions = JsonObject & { docsMode: boolean; compodoc: boolean; compodocArgs: string[]; - styles?: ExtraEntryPoint[]; + styles?: StyleElement[]; stylePreprocessorOptions?: StylePreprocessorOptions; } & Pick< // makes sure the option exists CLIOptions, - 'outputDir' | 'configDir' | 'loglevel' | 'quiet' | 'webpackStatsJson' + 'outputDir' | 'configDir' | 'loglevel' | 'quiet' | 'webpackStatsJson' | 'disableTelemetry' >; export type StorybookBuilderOutput = JsonObject & BuilderOutput & {}; +type StandaloneBuildOptions = StandaloneOptions & { outputDir: string }; + export default createBuilder(commandBuilder); function commandBuilder( @@ -65,15 +64,17 @@ function commandBuilder( outputDir, quiet, webpackStatsJson, + disableTelemetry, } = options; - const standaloneOptions: StandaloneOptions = { + const standaloneOptions: StandaloneBuildOptions = { packageJson: readUpSync({ cwd: __dirname }).packageJson, configDir, docsMode, loglevel, outputDir, quiet, + disableTelemetry, angularBrowserTarget: browserTarget, angularBuilderContext: context, angularBuilderOptions: { @@ -83,6 +84,7 @@ function commandBuilder( tsConfig, webpackStatsJson, }; + return standaloneOptions; }), switchMap((standaloneOptions) => runInstance({ ...standaloneOptions, mode: 'static' })), @@ -112,8 +114,15 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext) }; } -function runInstance(options: StandaloneOptions) { - return from(buildStaticStandalone(options as any)).pipe( - catchError((error: any) => throwError(buildStandaloneErrorHandler(error))) - ); +function runInstance(options: StandaloneBuildOptions) { + return from( + withTelemetry( + 'build', + { + cliOptions: options, + presetOptions: { ...options, corePresets: [], overridePresets: [] }, + }, + () => buildStaticStandalone(options) + ) + ).pipe(catchError((error: any) => throwError(buildStandaloneErrorHandler(error)))); } diff --git a/code/frameworks/angular/src/builders/build-storybook/schema.json b/code/frameworks/angular/src/builders/build-storybook/schema.json index 1640a3f986dd..29ebe3b5e19d 100644 --- a/code/frameworks/angular/src/builders/build-storybook/schema.json +++ b/code/frameworks/angular/src/builders/build-storybook/schema.json @@ -61,7 +61,7 @@ "type": "array", "description": "Global styles to be included in the build.", "items": { - "$ref": "#/definitions/extraEntryPoint" + "$ref": "#/definitions/styleElement" }, "default": "" }, @@ -83,7 +83,7 @@ }, "additionalProperties": false, "definitions": { - "extraEntryPoint": { + "styleElement": { "oneOf": [ { "type": "object", diff --git a/code/frameworks/angular/src/builders/start-storybook/index.ts b/code/frameworks/angular/src/builders/start-storybook/index.ts index 3d15453d9e51..6678cf501778 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.ts @@ -6,18 +6,15 @@ import { targetFromTargetString, } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; -import { - BrowserBuilderOptions, - ExtraEntryPoint, - StylePreprocessorOptions, -} from '@angular-devkit/build-angular'; +import { BrowserBuilderOptions, StylePreprocessorOptions } from '@angular-devkit/build-angular'; import { from, Observable, of } from 'rxjs'; import { CLIOptions } from '@storybook/types'; import { map, switchMap, mapTo } from 'rxjs/operators'; import { sync as findUpSync } from 'find-up'; import { sync as readUpSync } from 'read-pkg-up'; -import { buildDevStandalone } from '@storybook/core-server'; +import { buildDevStandalone, withTelemetry } from '@storybook/core-server'; +import { StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; import { StandaloneOptions } from '../utils/standalone-options'; import { runCompodoc } from '../utils/run-compodoc'; import { buildStandaloneErrorHandler } from '../utils/build-standalone-errors-handler'; @@ -28,7 +25,7 @@ export type StorybookBuilderOptions = JsonObject & { docsMode: boolean; compodoc: boolean; compodocArgs: string[]; - styles?: ExtraEntryPoint[]; + styles?: StyleElement[]; stylePreprocessorOptions?: StylePreprocessorOptions; } & Pick< // makes sure the option exists @@ -43,6 +40,7 @@ export type StorybookBuilderOptions = JsonObject & { | 'smokeTest' | 'ci' | 'quiet' + | 'disableTelemetry' >; export type StorybookBuilderOutput = JsonObject & BuilderOutput & {}; @@ -79,6 +77,7 @@ function commandBuilder( sslCa, sslCert, sslKey, + disableTelemetry, } = options; const standaloneOptions: StandaloneOptions = { @@ -94,6 +93,7 @@ function commandBuilder( sslCa, sslCert, sslKey, + disableTelemetry, angularBrowserTarget: browserTarget, angularBuilderContext: context, angularBuilderOptions: { @@ -134,9 +134,17 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext) function runInstance(options: StandaloneOptions) { return new Observable((observer) => { // This Observable intentionally never complete, leaving the process running ;) - buildDevStandalone(options as any).then( - ({ port }) => observer.next(port), - (error) => observer.error(buildStandaloneErrorHandler(error)) + withTelemetry( + 'dev', + { + cliOptions: options, + presetOptions: { ...options, corePresets: [], overridePresets: [] }, + }, + () => + buildDevStandalone(options).then( + ({ port }) => observer.next(port), + (error) => observer.error(buildStandaloneErrorHandler(error)) + ) ); }); } diff --git a/code/frameworks/angular/src/builders/start-storybook/schema.json b/code/frameworks/angular/src/builders/start-storybook/schema.json index 3c78f907bdd1..bfc83a59fe29 100644 --- a/code/frameworks/angular/src/builders/start-storybook/schema.json +++ b/code/frameworks/angular/src/builders/start-storybook/schema.json @@ -83,7 +83,7 @@ "type": "array", "description": "Global styles to be included in the build.", "items": { - "$ref": "#/definitions/extraEntryPoint" + "$ref": "#/definitions/styleElement" }, "default": "" }, @@ -105,7 +105,7 @@ }, "additionalProperties": false, "definitions": { - "extraEntryPoint": { + "styleElement": { "oneOf": [ { "type": "object", diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.ts index 2d9c99279910..ba509bd05abe 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.ts @@ -1,7 +1,7 @@ import { BuilderContext } from '@angular-devkit/architect'; -import { spawn } from 'child_process'; import { Observable } from 'rxjs'; import * as path from 'path'; +import { JsPackageManagerFactory } from '@storybook/cli'; const hasTsConfigArg = (args: string[]) => args.indexOf('-p') !== -1; const hasOutputArg = (args: string[]) => @@ -20,37 +20,22 @@ export const runCompodoc = ( return new Observable((observer) => { const tsConfigPath = toRelativePath(tsconfig); const finalCompodocArgs = [ - 'compodoc', - // Default options ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), - ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot}`]), + ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), ...compodocArgs, ]; - try { - context.logger.info(finalCompodocArgs.join(' ')); - const child = spawn('npx', finalCompodocArgs, { - cwd: context.workspaceRoot, - shell: true, - }); + const packageManager = JsPackageManagerFactory.getPackageManager(); - child.stdout.on('data', (data) => { - context.logger.info(data.toString()); - }); - child.stderr.on('data', (data) => { - context.logger.error(data.toString()); - }); + try { + const stdout = packageManager.runScript('compodoc', finalCompodocArgs, context.workspaceRoot); - child.on('close', (code) => { - if (code === 0) { - observer.next(); - observer.complete(); - } else { - observer.error(); - } - }); - } catch (error) { - observer.error(error); + context.logger.info(stdout); + observer.next(); + observer.complete(); + } catch (e) { + context.logger.error(e); + observer.error(); } }); }; diff --git a/code/frameworks/angular/src/builders/utils/standalone-options.ts b/code/frameworks/angular/src/builders/utils/standalone-options.ts index 8276dd26d8f8..69655957a924 100644 --- a/code/frameworks/angular/src/builders/utils/standalone-options.ts +++ b/code/frameworks/angular/src/builders/utils/standalone-options.ts @@ -1,17 +1,15 @@ import { BuilderContext } from '@angular-devkit/architect'; import { LoadOptions, CLIOptions, BuilderOptions } from '@storybook/types'; -export type StandaloneOptions = Partial< - CLIOptions & - LoadOptions & - BuilderOptions & { - mode?: 'static' | 'dev'; - angularBrowserTarget?: string | null; - angularBuilderOptions?: Record & { - styles?: any[]; - stylePreprocessorOptions?: any; - }; - angularBuilderContext?: BuilderContext | null; - tsConfig?: string; - } ->; +export type StandaloneOptions = CLIOptions & + LoadOptions & + BuilderOptions & { + mode?: 'static' | 'dev'; + angularBrowserTarget?: string | null; + angularBuilderOptions?: Record & { + styles?: any[]; + stylePreprocessorOptions?: any; + }; + angularBuilderContext?: BuilderContext | null; + tsConfig?: string; + }; diff --git a/code/frameworks/angular/src/client/angular-beta/AbstractRenderer.ts b/code/frameworks/angular/src/client/angular-beta/AbstractRenderer.ts index b4e52bf0068c..8caee3001496 100644 --- a/code/frameworks/angular/src/client/angular-beta/AbstractRenderer.ts +++ b/code/frameworks/angular/src/client/angular-beta/AbstractRenderer.ts @@ -1,43 +1,30 @@ -import { NgModule, PlatformRef, enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { NgModule, enableProdMode, Type, ApplicationRef } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; import { Subject, BehaviorSubject } from 'rxjs'; import { stringify } from 'telejson'; import { ICollection, StoryFnAngularReturnType, Parameters } from '../types'; -import { createStorybookModule, getStorybookModuleMetadata } from './StorybookModule'; +import { getApplication } from './StorybookModule'; +import { storyPropsProvider } from './StorybookProvider'; +import { componentNgModules } from './StorybookWrapperComponent'; type StoryRenderInfo = { storyFnAngular: StoryFnAngularReturnType; moduleMetadataSnapshot: string; }; -// platform must be init only if render is called at least once -let platformRef: PlatformRef; -function getPlatform(newPlatform?: boolean): PlatformRef { - if (!platformRef || newPlatform) { - platformRef = platformBrowserDynamic(); - } - return platformRef; -} +const applicationRefs = new Set(); export abstract class AbstractRenderer { /** * Wait and destroy the platform */ - public static resetPlatformBrowserDynamic() { - return new Promise((resolve) => { - if (platformRef && !platformRef.destroyed) { - platformRef.onDestroy(async () => { - resolve(); - }); - // Destroys the current Angular platform and all Angular applications on the page. - // So call each angular ngOnDestroy and avoid memory leaks - platformRef.destroy(); - return; + public static resetApplications() { + componentNgModules.clear(); + applicationRefs.forEach((appRef) => { + if (!appRef.destroyed) { + appRef.destroy(); } - resolve(); - }).then(() => { - getPlatform(true); }); } @@ -109,15 +96,13 @@ export abstract class AbstractRenderer { const targetSelector = `${this.generateTargetSelectorFromStoryId()}`; const newStoryProps$ = new BehaviorSubject(storyFnAngular.props); - const moduleMetadata = getStorybookModuleMetadata( - { storyFnAngular, component, targetSelector }, - newStoryProps$ - ); if ( !this.fullRendererRequired({ storyFnAngular, - moduleMetadata, + moduleMetadata: { + ...storyFnAngular.moduleMetadata, + }, forced, }) ) { @@ -125,7 +110,6 @@ export abstract class AbstractRenderer { return; } - await this.beforeFullRender(); // Complete last BehaviorSubject and set a new one for the current module if (this.storyProps$) { @@ -135,10 +119,14 @@ export abstract class AbstractRenderer { this.initAngularRootElement(targetDOMNode, targetSelector); - await getPlatform().bootstrapModule( - createStorybookModule(moduleMetadata), - parameters.bootstrapModuleOptions ?? undefined - ); + const application = getApplication({ storyFnAngular, component, targetSelector }); + + const applicationRef = await bootstrapApplication(application, { + providers: [storyPropsProvider(newStoryProps$)], + }); + + applicationRefs.add(applicationRef); + await this.afterFullRender(); } diff --git a/code/frameworks/angular/src/client/angular-beta/CanvasRenderer.ts b/code/frameworks/angular/src/client/angular-beta/CanvasRenderer.ts index df1eb5f40436..994733657f78 100644 --- a/code/frameworks/angular/src/client/angular-beta/CanvasRenderer.ts +++ b/code/frameworks/angular/src/client/angular-beta/CanvasRenderer.ts @@ -13,7 +13,7 @@ export class CanvasRenderer extends AbstractRenderer { } async beforeFullRender(): Promise { - await CanvasRenderer.resetPlatformBrowserDynamic(); + CanvasRenderer.resetApplications(); } async afterFullRender(): Promise { diff --git a/code/frameworks/angular/src/client/angular-beta/DocsRenderer.ts b/code/frameworks/angular/src/client/angular-beta/DocsRenderer.ts index 0e1de2489886..945881088b82 100644 --- a/code/frameworks/angular/src/client/angular-beta/DocsRenderer.ts +++ b/code/frameworks/angular/src/client/angular-beta/DocsRenderer.ts @@ -23,7 +23,7 @@ export class DocsRenderer extends AbstractRenderer { * */ channel.once(STORY_CHANGED, async () => { - await DocsRenderer.resetPlatformBrowserDynamic(); + await DocsRenderer.resetApplications(); }); /** @@ -32,13 +32,15 @@ export class DocsRenderer extends AbstractRenderer { * for previous component */ channel.once(DOCS_RENDERED, async () => { - await DocsRenderer.resetPlatformBrowserDynamic(); + await DocsRenderer.resetApplications(); }); await super.render({ ...options, forced: false }); } - async beforeFullRender(): Promise {} + async beforeFullRender(): Promise { + DocsRenderer.resetApplications(); + } async afterFullRender(): Promise { await AbstractRenderer.resetCompiledComponents(); diff --git a/code/frameworks/angular/src/client/angular-beta/RendererFactory.ts b/code/frameworks/angular/src/client/angular-beta/RendererFactory.ts index 888024d83614..9ae768c57c29 100644 --- a/code/frameworks/angular/src/client/angular-beta/RendererFactory.ts +++ b/code/frameworks/angular/src/client/angular-beta/RendererFactory.ts @@ -22,7 +22,7 @@ export class RendererFactory { const renderType = getRenderType(targetDOMNode); // keep only instances of the same type if (this.lastRenderType && this.lastRenderType !== renderType) { - await AbstractRenderer.resetPlatformBrowserDynamic(); + await AbstractRenderer.resetApplications(); clearRootHTMLElement(renderType); this.rendererMap.clear(); } diff --git a/code/frameworks/angular/src/client/angular-beta/StorybookModule.test.ts b/code/frameworks/angular/src/client/angular-beta/StorybookModule.test.ts index 513e72bfde4f..8ee95e2e4558 100644 --- a/code/frameworks/angular/src/client/angular-beta/StorybookModule.test.ts +++ b/code/frameworks/angular/src/client/angular-beta/StorybookModule.test.ts @@ -4,7 +4,8 @@ import { TestBed } from '@angular/core/testing'; import { BrowserModule } from '@angular/platform-browser'; import { BehaviorSubject } from 'rxjs'; import { ICollection } from '../types'; -import { getStorybookModuleMetadata } from './StorybookModule'; +import { getApplication } from './StorybookModule'; +import { storyPropsProvider } from './StorybookProvider'; describe('StorybookModule', () => { describe('getStorybookModuleMetadata', () => { @@ -54,16 +55,16 @@ describe('StorybookModule', () => { localFunction: () => 'localFunction', }; - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { props }, - component: FooComponent, - targetSelector: 'my-selector', - }, - new BehaviorSubject(props) - ); + const application = getApplication({ + storyFnAngular: { props }, + component: FooComponent, + targetSelector: 'my-selector', + }); - const { fixture } = await configureTestingModule(ngModule); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(new BehaviorSubject(props))], + }); fixture.detectChanges(); expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(props.input); @@ -90,16 +91,16 @@ describe('StorybookModule', () => { }, }; - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { props }, - component: FooComponent, - targetSelector: 'my-selector', - }, - new BehaviorSubject(props) - ); + const application = getApplication({ + storyFnAngular: { props }, + component: FooComponent, + targetSelector: 'my-selector', + }); - const { fixture } = await configureTestingModule(ngModule); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(new BehaviorSubject(props))], + }); fixture.detectChanges(); fixture.nativeElement.querySelector('p#output').click(); @@ -116,15 +117,15 @@ describe('StorybookModule', () => { }; const storyProps$ = new BehaviorSubject(initialProps); - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { props: initialProps }, - component: FooComponent, - targetSelector: 'my-selector', - }, - storyProps$ - ); - const { fixture } = await configureTestingModule(ngModule); + const application = getApplication({ + storyFnAngular: { props: initialProps }, + component: FooComponent, + targetSelector: 'my-selector', + }); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(storyProps$)], + }); fixture.detectChanges(); expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual( @@ -169,15 +170,15 @@ describe('StorybookModule', () => { }; const storyProps$ = new BehaviorSubject(initialProps); - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { props: initialProps }, - component: FooComponent, - targetSelector: 'my-selector', - }, - storyProps$ - ); - const { fixture } = await configureTestingModule(ngModule); + const application = getApplication({ + storyFnAngular: { props: initialProps }, + component: FooComponent, + targetSelector: 'my-selector', + }); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(storyProps$)], + }); fixture.detectChanges(); const newProps = { @@ -207,18 +208,18 @@ describe('StorybookModule', () => { }; const storyProps$ = new BehaviorSubject(initialProps); - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { - props: initialProps, - template: '

', - }, - component: FooComponent, - targetSelector: 'my-selector', + const application = getApplication({ + storyFnAngular: { + props: initialProps, + template: '

', }, - storyProps$ - ); - const { fixture } = await configureTestingModule(ngModule); + component: FooComponent, + targetSelector: 'my-selector', + }); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(storyProps$)], + }); fixture.detectChanges(); expect(fixture.nativeElement.querySelector('p').style.color).toEqual('red'); expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual( @@ -242,15 +243,16 @@ describe('StorybookModule', () => { }; const storyProps$ = new BehaviorSubject(initialProps); - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { props: initialProps }, - component: FooComponent, - targetSelector: 'my-selector', - }, - storyProps$ - ); - const { fixture } = await configureTestingModule(ngModule); + const application = getApplication({ + storyFnAngular: { props: initialProps }, + component: FooComponent, + targetSelector: 'my-selector', + }); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(storyProps$)], + }); + fixture.detectChanges(); expect(fixture.nativeElement.querySelector('p#setterCallNb').innerHTML).toEqual('1'); @@ -274,19 +276,19 @@ describe('StorybookModule', () => { it('should display the component', async () => { const props = {}; - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { - props, - moduleMetadata: { entryComponents: [WithoutSelectorComponent] }, - }, - component: WithoutSelectorComponent, - targetSelector: 'my-selector', + const application = getApplication({ + storyFnAngular: { + props, + moduleMetadata: { entryComponents: [WithoutSelectorComponent] }, }, - new BehaviorSubject(props) - ); + component: WithoutSelectorComponent, + targetSelector: 'my-selector', + }); - const { fixture } = await configureTestingModule(ngModule); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(new BehaviorSubject(props))], + }); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('The content'); @@ -300,16 +302,16 @@ describe('StorybookModule', () => { }) class FooComponent {} - const ngModule = getStorybookModuleMetadata( - { - storyFnAngular: { template: '' }, - component: FooComponent, - targetSelector: 'my-selector', - }, - new BehaviorSubject({}) - ); + const application = getApplication({ + storyFnAngular: { template: '' }, + component: FooComponent, + targetSelector: 'my-selector', + }); - const { fixture } = await configureTestingModule(ngModule); + const { fixture } = await configureTestingModule({ + imports: [application], + providers: [storyPropsProvider(new BehaviorSubject({}))], + }); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); @@ -317,18 +319,9 @@ describe('StorybookModule', () => { }); async function configureTestingModule(ngModule: NgModule) { - await TestBed.configureTestingModule({ - declarations: ngModule.declarations, - providers: ngModule.providers, - }) - .overrideModule(BrowserModule, { - set: { - entryComponents: [...ngModule.entryComponents], - }, - }) - .compileComponents(); + await TestBed.configureTestingModule(ngModule).compileComponents(); - const fixture = TestBed.createComponent(ngModule.bootstrap[0] as Type); + const fixture = TestBed.createComponent(ngModule.imports[0] as any); return { fixture, diff --git a/code/frameworks/angular/src/client/angular-beta/StorybookModule.ts b/code/frameworks/angular/src/client/angular-beta/StorybookModule.ts index 52d07baacd01..2f43390d806d 100644 --- a/code/frameworks/angular/src/client/angular-beta/StorybookModule.ts +++ b/code/frameworks/angular/src/client/angular-beta/StorybookModule.ts @@ -1,26 +1,16 @@ -import { Type, NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; - -import { Subject } from 'rxjs'; -import { ICollection, StoryFnAngularReturnType } from '../types'; -import { storyPropsProvider } from './StorybookProvider'; -import { isComponentAlreadyDeclaredInModules } from './utils/NgModulesAnalyzer'; -import { isDeclarable, isStandaloneComponent } from './utils/NgComponentAnalyzer'; +import { StoryFnAngularReturnType } from '../types'; import { createStorybookWrapperComponent } from './StorybookWrapperComponent'; import { computesTemplateFromComponent } from './ComputesTemplateFromComponent'; -export const getStorybookModuleMetadata = ( - { - storyFnAngular, - component, - targetSelector, - }: { - storyFnAngular: StoryFnAngularReturnType; - component?: any; - targetSelector: string; - }, - storyProps$: Subject -): NgModule => { +export const getApplication = ({ + storyFnAngular, + component, + targetSelector, +}: { + storyFnAngular: StoryFnAngularReturnType; + component?: any; + targetSelector: string; +}) => { const { props, styles, moduleMetadata = {} } = storyFnAngular; let { template } = storyFnAngular; @@ -32,47 +22,14 @@ export const getStorybookModuleMetadata = ( /** * Create a component that wraps generated template and gives it props */ - const ComponentToInject = createStorybookWrapperComponent( + return createStorybookWrapperComponent( targetSelector, template, component, styles, + moduleMetadata, props ); - - const isStandalone = isStandaloneComponent(component); - // Look recursively (deep) if the component is not already declared by an import module - const requiresComponentDeclaration = - isDeclarable(component) && - !isComponentAlreadyDeclaredInModules( - component, - moduleMetadata.declarations, - moduleMetadata.imports - ) && - !isStandalone; - - return { - declarations: [ - ...(requiresComponentDeclaration ? [component] : []), - ComponentToInject, - ...(moduleMetadata.declarations ?? []), - ], - imports: [ - BrowserModule, - ...(isStandalone ? [component] : []), - ...(moduleMetadata.imports ?? []), - ], - providers: [storyPropsProvider(storyProps$), ...(moduleMetadata.providers ?? [])], - entryComponents: [...(moduleMetadata.entryComponents ?? [])], - schemas: [...(moduleMetadata.schemas ?? [])], - bootstrap: [ComponentToInject], - }; -}; - -export const createStorybookModule = (ngModule: NgModule): Type => { - @NgModule(ngModule) - class StorybookModule {} - return StorybookModule; }; function hasNoTemplate(template: string | null | undefined): template is undefined { diff --git a/code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts b/code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts index 79e6d3e23e84..9c3efc087219 100644 --- a/code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts +++ b/code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { AfterViewInit, ElementRef, @@ -8,13 +9,20 @@ import { Inject, ViewChild, ViewContainerRef, + NgModule, } from '@angular/core'; import { Subscription, Subject } from 'rxjs'; import { map, skip } from 'rxjs/operators'; -import { ICollection } from '../types'; +import { ICollection, NgModuleMetadata } from '../types'; import { STORY_PROPS } from './StorybookProvider'; -import { ComponentInputsOutputs, getComponentInputsOutputs } from './utils/NgComponentAnalyzer'; +import { + ComponentInputsOutputs, + getComponentInputsOutputs, + isDeclarable, + isStandaloneComponent, +} from './utils/NgComponentAnalyzer'; +import { isComponentAlreadyDeclaredInModules } from './utils/NgModulesAnalyzer'; const getNonInputsOutputsProps = ( ngComponentInputsOutputs: ComponentInputsOutputs, @@ -29,6 +37,8 @@ const getNonInputsOutputsProps = ( return Object.keys(props).filter((k) => ![...inputs, ...outputs].includes(k)); }; +export const componentNgModules = new Map>(); + /** * Wraps the story template into a component * @@ -40,16 +50,65 @@ export const createStorybookWrapperComponent = ( template: string, storyComponent: Type | undefined, styles: string[], + moduleMetadata: NgModuleMetadata, initialProps?: ICollection ): Type => { // In ivy, a '' selector is not allowed, therefore we need to just set it to anything if // storyComponent was not provided. const viewChildSelector = storyComponent ?? '__storybook-noop'; + const isStandalone = isStandaloneComponent(storyComponent); + // Look recursively (deep) if the component is not already declared by an import module + const requiresComponentDeclaration = + isDeclarable(storyComponent) && + !isComponentAlreadyDeclaredInModules( + storyComponent, + moduleMetadata.declarations, + moduleMetadata.imports + ) && + !isStandalone; + + const providersNgModules = (moduleMetadata.providers ?? []).map((provider) => { + if (!componentNgModules.get(provider)) { + @NgModule({ + providers: [provider], + }) + class ProviderModule {} + + componentNgModules.set(provider, ProviderModule); + } + + return componentNgModules.get(provider); + }); + + if (!componentNgModules.get(storyComponent)) { + const declarations = [ + ...(requiresComponentDeclaration ? [storyComponent] : []), + ...(moduleMetadata.declarations ?? []), + ]; + + @NgModule({ + declarations, + imports: [CommonModule, ...(moduleMetadata.imports ?? [])], + exports: [...declarations, ...(moduleMetadata.imports ?? [])], + }) + class StorybookComponentModule {} + + componentNgModules.set(storyComponent, StorybookComponentModule); + } + @Component({ selector, template, + standalone: true, + imports: [ + CommonModule, + componentNgModules.get(storyComponent), + ...providersNgModules, + ...(isStandalone ? [storyComponent] : []), + ], styles, + schemas: moduleMetadata.schemas, }) class StorybookWrapperComponent implements AfterViewInit, OnDestroy { private storyComponentPropsSubscription: Subscription; diff --git a/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.test.ts b/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.test.ts index 95f4858fe944..5bb62cc8a0c6 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.test.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.test.ts @@ -1,5 +1,4 @@ import { - ComponentFactory, Type, Component, ComponentFactoryResolver, @@ -238,18 +237,14 @@ describe('isComponent', () => { describe('isStandaloneComponent', () => { it('should return true with a Component with "standalone: true"', () => { - // TODO: `standalone` is only available in Angular v14. Remove cast to `any` once - // Angular deps are updated to v14.x.x. - @Component({ standalone: true } as any) + @Component({ standalone: true }) class FooComponent {} expect(isStandaloneComponent(FooComponent)).toEqual(true); }); it('should return false with a Component with "standalone: false"', () => { - // TODO: `standalone` is only available in Angular v14. Remove cast to `any` once - // Angular deps are updated to v14.x.x. - @Component({ standalone: false } as any) + @Component({ standalone: false }) class FooComponent {} expect(isStandaloneComponent(FooComponent)).toEqual(false); @@ -269,18 +264,14 @@ describe('isStandaloneComponent', () => { }); it('should return true with a Directive with "standalone: true"', () => { - // TODO: `standalone` is only available in Angular v14. Remove cast to `any` once - // Angular deps are updated to v14.x.x. - @Directive({ standalone: true } as any) + @Directive({ standalone: true }) class FooDirective {} expect(isStandaloneComponent(FooDirective)).toEqual(true); }); it('should return false with a Directive with "standalone: false"', () => { - // TODO: `standalone` is only available in Angular v14. Remove cast to `any` once - // Angular deps are updated to v14.x.x. - @Directive({ standalone: false } as any) + @Directive({ standalone: false }) class FooDirective {} expect(isStandaloneComponent(FooDirective)).toEqual(false); @@ -294,18 +285,14 @@ describe('isStandaloneComponent', () => { }); it('should return true with a Pipe with "standalone: true"', () => { - // TODO: `standalone` is only available in Angular v14. Remove cast to `any` once - // Angular deps are updated to v14.x.x. - @Pipe({ standalone: true } as any) + @Pipe({ name: 'FooPipe', standalone: true }) class FooPipe {} expect(isStandaloneComponent(FooPipe)).toEqual(true); }); it('should return false with a Pipe with "standalone: false"', () => { - // TODO: `standalone` is only available in Angular v14. Remove cast to `any` once - // Angular deps are updated to v14.x.x. - @Pipe({ standalone: false } as any) + @Pipe({ name: 'FooPipe', standalone: false }) class FooPipe {} expect(isStandaloneComponent(FooPipe)).toEqual(false); @@ -356,7 +343,7 @@ function sortByPropName( return array.sort((a, b) => a.propName.localeCompare(b.propName)); } -function resolveComponentFactory>(component: T): ComponentFactory { +function resolveComponentFactory>(component: T) { TestBed.configureTestingModule({ declarations: [component], }).overrideModule(BrowserDynamicTestingModule, { diff --git a/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts b/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts index 43219ace1f31..7c31a93d2787 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts @@ -117,9 +117,7 @@ export const isStandaloneComponent = (component: any): component is Type - (d instanceof Component || d instanceof Directive || d instanceof Pipe) && - (d as any).standalone + (d) => (d instanceof Component || d instanceof Directive || d instanceof Pipe) && d.standalone ); }; diff --git a/code/frameworks/angular/src/client/angular/helpers.ts b/code/frameworks/angular/src/client/angular/helpers.ts deleted file mode 100644 index 009a02298f9c..000000000000 --- a/code/frameworks/angular/src/client/angular/helpers.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { global } from '@storybook/global'; -import { NgModuleRef, Type, enableProdMode, NgModule, Component, NgZone } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { BrowserModule } from '@angular/platform-browser'; -import { Subscriber, Observable, ReplaySubject } from 'rxjs'; -import { PartialStoryFn } from '@storybook/types'; -import { AppComponent } from './app.component'; -import { STORY } from './app.token'; -import { NgModuleMetadata, StoryFnAngularReturnType, AngularRenderer } from '../types'; - -const { document } = global; - -declare global { - interface Window { - NODE_ENV: 'string' | 'development' | undefined; - } -} - -let platform: any = null; -let promises: Promise>[] = []; -let storyData = new ReplaySubject(1); - -const moduleClass = class DynamicModule {}; -const componentClass = class DynamicComponent {}; - -type DynamicComponentType = typeof componentClass; - -function storyDataFactory(data: Observable) { - return (ngZone: NgZone) => - new Observable((subscriber: Subscriber) => { - const sub = data.subscribe( - (v: T) => { - ngZone.run(() => subscriber.next(v)); - }, - (err) => { - ngZone.run(() => subscriber.error(err)); - }, - () => { - ngZone.run(() => subscriber.complete()); - } - ); - - return () => { - sub.unsubscribe(); - }; - }); -} - -const getModule = ( - declarations: (Type | any[])[], - entryComponents: (Type | any[])[], - bootstrap: (Type | any[])[], - data: StoryFnAngularReturnType, - moduleMetadata: NgModuleMetadata -) => { - // Complete last ReplaySubject and create a new one for the current module - storyData.complete(); - storyData = new ReplaySubject(1); - storyData.next(data); - - const moduleMeta = { - declarations: [...declarations, ...(moduleMetadata.declarations || [])], - imports: [BrowserModule, FormsModule, ...(moduleMetadata.imports || [])], - providers: [ - { provide: STORY, useFactory: storyDataFactory(storyData.asObservable()), deps: [NgZone] }, - ...(moduleMetadata.providers || []), - ], - entryComponents: [...entryComponents, ...(moduleMetadata.entryComponents || [])], - schemas: [...(moduleMetadata.schemas || [])], - bootstrap: [...bootstrap], - }; - - return NgModule(moduleMeta)(moduleClass); -}; - -const createComponentFromTemplate = (template: string, styles: string[]) => { - return Component({ - template, - styles, - })(componentClass); -}; - -const extractNgModuleMetadata = (importItem: any): NgModule => { - const target = importItem && importItem.ngModule ? importItem.ngModule : importItem; - const decoratorKey = '__annotations__'; - const decorators: any[] = - Reflect && - Reflect.getOwnPropertyDescriptor && - Reflect.getOwnPropertyDescriptor(target, decoratorKey) - ? Reflect.getOwnPropertyDescriptor(target, decoratorKey).value - : target[decoratorKey]; - - if (!decorators || decorators.length === 0) { - return null; - } - - const ngModuleDecorator: NgModule | undefined = decorators.find( - (decorator) => decorator instanceof NgModule - ); - if (!ngModuleDecorator) { - return null; - } - return ngModuleDecorator; -}; - -const getExistenceOfComponentInModules = ( - component: any, - declarations: any[], - imports: any[] -): boolean => { - if (declarations && declarations.some((declaration) => declaration === component)) { - // Found component in declarations array - return true; - } - if (!imports) { - return false; - } - - return imports.some((importItem) => { - const extractedNgModuleMetadata = extractNgModuleMetadata(importItem); - if (!extractedNgModuleMetadata) { - // Not an NgModule - return false; - } - return getExistenceOfComponentInModules( - component, - extractedNgModuleMetadata.declarations, - extractedNgModuleMetadata.imports - ); - }); -}; - -const initModule = (storyFn: PartialStoryFn) => { - const storyObj = storyFn(); - const { component, template, props, styles, moduleMetadata = {} } = storyObj; - - const isCreatingComponentFromTemplate = Boolean(template); - - const AnnotatedComponent = isCreatingComponentFromTemplate - ? createComponentFromTemplate(template, styles) - : component; - - const componentRequiresDeclaration = - isCreatingComponentFromTemplate || - !getExistenceOfComponentInModules( - component, - moduleMetadata.declarations, - moduleMetadata.imports - ); - - const componentDeclarations = componentRequiresDeclaration - ? [AppComponent, AnnotatedComponent] - : [AppComponent]; - - const story = { - component: AnnotatedComponent, - props, - }; - - return getModule( - componentDeclarations, - [AnnotatedComponent], - [AppComponent], - story, - moduleMetadata - ); -}; - -const staticRoot = document.getElementById('storybook-root'); -const insertDynamicRoot = () => { - const app = document.createElement('storybook-dynamic-app-root'); - staticRoot.innerHTML = ''; - staticRoot.appendChild(app); -}; - -const draw = (newModule: DynamicComponentType): void => { - if (!platform) { - insertDynamicRoot(); - if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') { - try { - enableProdMode(); - } catch (e) { - // - } - } - - platform = platformBrowserDynamic(); - promises.push(platform.bootstrapModule(newModule)); - } else { - Promise.all(promises).then((modules) => { - modules.forEach((mod) => mod.destroy()); - - insertDynamicRoot(); - promises = []; - promises.push(platform.bootstrapModule(newModule)); - }); - } -}; - -export const renderNgApp = (storyFn: PartialStoryFn, forced: boolean) => { - if (!forced) { - draw(initModule(storyFn)); - } else { - storyData.next(storyFn()); - } -}; diff --git a/code/frameworks/angular/src/client/decorateStory.ts b/code/frameworks/angular/src/client/decorateStory.ts index 532b4353f7fb..83620080a625 100644 --- a/code/frameworks/angular/src/client/decorateStory.ts +++ b/code/frameworks/angular/src/client/decorateStory.ts @@ -34,7 +34,7 @@ const prepareMain = ( ): AngularRenderer['storyResult'] => { let { template } = story; - const component = story.component ?? context.component; + const { component } = context; const userDefinedTemplate = !hasNoTemplate(template); if (!userDefinedTemplate && component) { diff --git a/code/frameworks/angular/src/client/render.ts b/code/frameworks/angular/src/client/render.ts index 5dc7c6e37a0c..b096eb6caf2e 100644 --- a/code/frameworks/angular/src/client/render.ts +++ b/code/frameworks/angular/src/client/render.ts @@ -1,6 +1,7 @@ +import '@angular/compiler'; + import { RenderContext, ArgsStoryFn } from '@storybook/types'; -import { renderNgApp } from './angular/helpers'; import { AngularRenderer } from './types'; import { RendererFactory } from './angular-beta/RendererFactory'; @@ -21,11 +22,6 @@ export async function renderToCanvas( ) { showMain(); - if (parameters.angularLegacyRendering) { - renderNgApp(storyFn, !forceRemount); - return; - } - const renderer = await rendererFactory.getRendererInstance(id, element); await renderer.render({ diff --git a/code/frameworks/angular/src/client/types.ts b/code/frameworks/angular/src/client/types.ts index 14f41557d5d5..2818ec33353f 100644 --- a/code/frameworks/angular/src/client/types.ts +++ b/code/frameworks/angular/src/client/types.ts @@ -37,8 +37,6 @@ export interface AngularRenderer extends WebRenderer { } export type Parameters = DefaultParameters & { - /** Uses legacy angular rendering engine that use dynamic component */ - angularLegacyRendering?: boolean; bootstrapModuleOptions?: unknown; }; diff --git a/code/frameworks/angular/src/renderer.ts b/code/frameworks/angular/src/renderer.ts index 3216885b6574..11d729059f2f 100644 --- a/code/frameworks/angular/src/renderer.ts +++ b/code/frameworks/angular/src/renderer.ts @@ -1,4 +1,5 @@ +export { storyPropsProvider } from './client/angular-beta/StorybookProvider'; export { computesTemplateSourceFromComponent } from './client/angular-beta/ComputesTemplateFromComponent'; export { rendererFactory } from './client/render'; export { AbstractRenderer } from './client/angular-beta/AbstractRenderer'; -export { getStorybookModuleMetadata } from './client/angular-beta/StorybookModule'; +export { getApplication } from './client/angular-beta/StorybookModule'; diff --git a/code/frameworks/angular/src/server/angular-read-workspace.ts b/code/frameworks/angular/src/server/angular-read-workspace.ts deleted file mode 100644 index e22ceb99f2f0..000000000000 --- a/code/frameworks/angular/src/server/angular-read-workspace.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable global-require */ -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { workspaces } from '@angular-devkit/core'; - -/** - * Returns the workspace definition - * - * - Either from NX if it is present - * - Either from `@angular-devkit/core` -> https://github.com/angular/angular-cli/tree/master/packages/angular_devkit/core - */ -export const readAngularWorkspaceConfig = async ( - dirToSearch: string -): Promise => { - const host = workspaces.createWorkspaceHost(new NodeJsSyncHost()); - - try { - /** - * Apologies for the following line - * If there's a better way to do it, let's do it - */ - - // catch if nx.json does not exist - require('@nrwl/workspace').readNxJson(); - - const nxWorkspace = require('@nrwl/workspace').readWorkspaceConfig({ - format: 'angularCli', - path: dirToSearch, - }); - - // Use the workspace version of nx when angular looks for the angular.json file - host.readFile = (path) => { - if (typeof path === 'string' && path.endsWith('angular.json')) { - return Promise.resolve(JSON.stringify(nxWorkspace)); - } - return host.readFile(path); - }; - host.isFile = (path) => { - if (typeof path === 'string' && path.endsWith('angular.json')) { - return Promise.resolve(true); - } - return host.isFile(path); - }; - } catch (e) { - // Ignore if the client does not use NX - } - - return (await workspaces.readWorkspace(dirToSearch, host)).workspace; -}; - -export const getDefaultProjectName = (workspace: workspaces.WorkspaceDefinition): string => { - const environmentProjectName = process.env.STORYBOOK_ANGULAR_PROJECT; - if (environmentProjectName) { - return environmentProjectName; - } - - if (workspace.projects.has('storybook')) { - return 'storybook'; - } - if (workspace.extensions.defaultProject) { - return workspace.extensions.defaultProject as string; - } - - const firstProjectName = workspace.projects.keys().next().value; - if (firstProjectName) { - return firstProjectName; - } - throw new Error('No angular projects found'); -}; - -export const findAngularProjectTarget = ( - workspace: workspaces.WorkspaceDefinition, - projectName: string, - targetName: string -): { - project: workspaces.ProjectDefinition; - target: workspaces.TargetDefinition; -} => { - if (!workspace.projects || !Object.keys(workspace.projects).length) { - throw new Error('No angular projects found'); - } - - const project = workspace.projects.get(projectName); - - if (!project) { - throw new Error(`"${projectName}" project is not found in angular.json`); - } - - if (!project.targets.has(targetName)) { - throw new Error(`"${targetName}" target is not found in "${projectName}" project`); - } - const target = project.targets.get(targetName); - - return { project, target }; -}; diff --git a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts index 0af9a9217bf6..f8f78996c366 100644 --- a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts +++ b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts @@ -1,19 +1,13 @@ import webpack from 'webpack'; import { logger } from '@storybook/node-logger'; -import { BuilderContext, Target, targetFromTargetString } from '@angular-devkit/architect'; +import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; import { sync as findUpSync } from 'find-up'; -import semver from 'semver'; import { dedent } from 'ts-dedent'; import { JsonObject, logging } from '@angular-devkit/core'; import { getWebpackConfig as getCustomWebpackConfig } from './angular-cli-webpack'; import { moduleIsAvailable } from './utils/module-is-available'; import { PresetOptions } from './preset-options'; -import { - getDefaultProjectName, - findAngularProjectTarget, - readAngularWorkspaceConfig, -} from './angular-read-workspace'; export async function webpackFinal(baseConfig: webpack.Configuration, options: PresetOptions) { if (!moduleIsAvailable('@angular-devkit/build-angular')) { @@ -21,43 +15,18 @@ export async function webpackFinal(baseConfig: webpack.Configuration, options: P return baseConfig; } - const angularCliVersion = await import('@angular/cli').then((m) => semver.coerce(m.VERSION.full)); + checkForLegacyBuildOptions(options); - /** - * Ordered array to use the specific getWebpackConfig according to some condition like angular-cli version - */ - const webpackGetterByVersions: { - info: string; - condition: boolean; - getWebpackConfig( - baseConfig: webpack.Configuration, - options: PresetOptions - ): Promise | webpack.Configuration; - }[] = [ - { - info: '=> Loading angular-cli config for angular >= 13.0.0', - condition: semver.satisfies(angularCliVersion, '>=13.0.0'), - getWebpackConfig: async (_baseConfig, _options) => { - const builderContext = getBuilderContext(_options); - const builderOptions = await getBuilderOptions(_options, builderContext); - const legacyDefaultOptions = await getLegacyDefaultBuildOptions(_options); + const builderContext = getBuilderContext(options); + const builderOptions = await getBuilderOptions(options, builderContext); - return getCustomWebpackConfig(_baseConfig, { - builderOptions: { - watch: options.configType === 'DEVELOPMENT', - ...legacyDefaultOptions, - ...builderOptions, - }, - builderContext, - }); - }, + return getCustomWebpackConfig(baseConfig, { + builderOptions: { + watch: options.configType === 'DEVELOPMENT', + ...builderOptions, }, - ]; - - const webpackGetter = webpackGetterByVersions.find((wg) => wg.condition); - - logger.info(webpackGetter.info); - return Promise.resolve(webpackGetter.getWebpackConfig(baseConfig, options)); + builderContext, + }); } /** @@ -116,58 +85,23 @@ async function getBuilderOptions( return builderOptions; } +export const migrationToBuilderReferrenceMessage = dedent`Your Storybook startup uses a solution that is not supported. + You must use angular builder to have an explicit configuration on the project used in angular.json + Read more at: + - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#sb-angular-builder) + - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#angular13) + `; + /** - * Get options from legacy way - * /!\ This is only for backward compatibility and would be removed on Storybook 7.0 - * only work for angular.json with [defaultProject].build or "storybook.build" config + * Checks if using legacy configuration that doesn't use builder and logs message referring to migration docs. */ -async function getLegacyDefaultBuildOptions(options: PresetOptions) { +function checkForLegacyBuildOptions(options: PresetOptions) { if (options.angularBrowserTarget !== undefined) { // Not use legacy way with builder (`angularBrowserTarget` is defined or null with builder and undefined without) - return {}; - } - - logger.warn(dedent`Your Storybook startup uses a solution that will not be supported in version 7.0. - You must use angular builder to have an explicit configuration on the project used in angular.json - Read more at: - - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#sb-angular-builder) - - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#angular13) - `); - const dirToSearch = process.cwd(); - - // Read angular workspace - let workspaceConfig; - try { - workspaceConfig = await readAngularWorkspaceConfig(dirToSearch); - } catch (error) { - logger.error( - `=> Could not find angular workspace config (angular.json) on this path "${dirToSearch}"` - ); - logger.info(`=> Fail to load angular-cli config. Using base config`); - return {}; + return; } - // Find angular project target - try { - const browserTarget = { - configuration: undefined, - project: getDefaultProjectName(workspaceConfig), - target: 'build', - } as Target; - - const { target } = findAngularProjectTarget( - workspaceConfig, - browserTarget.project, - browserTarget.target - ); + logger.error(migrationToBuilderReferrenceMessage); - logger.info( - `=> Using angular project "${browserTarget.project}:${browserTarget.target}" for configuring Storybook` - ); - return { ...target.options }; - } catch (error) { - logger.error(`=> Could not find angular project: ${error.message}`); - logger.info(`=> Fail to load angular-cli config. Using base config`); - return {}; - } + throw Error('angularBrowserTarget is undefined.'); } diff --git a/code/frameworks/angular/src/server/preset-options.ts b/code/frameworks/angular/src/server/preset-options.ts index a831d206f861..5412d5a19482 100644 --- a/code/frameworks/angular/src/server/preset-options.ts +++ b/code/frameworks/angular/src/server/preset-options.ts @@ -1,14 +1,15 @@ import { Options as CoreOptions } from '@storybook/types'; import { BuilderContext } from '@angular-devkit/architect'; -import { ExtraEntryPoint, StylePreprocessorOptions } from '@angular-devkit/build-angular'; +import { StylePreprocessorOptions } from '@angular-devkit/build-angular'; +import { StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; export type PresetOptions = CoreOptions & { /* Allow to get the options of a targeted "browser builder" */ angularBrowserTarget?: string | null; /* Defined set of options. These will take over priority from angularBrowserTarget options */ angularBuilderOptions?: { - styles?: ExtraEntryPoint[]; + styles?: StyleElement[]; stylePreprocessorOptions?: StylePreprocessorOptions; }; /* Angular context from builder */ diff --git a/code/frameworks/angular/src/server/utils/normalize-asset-patterns.ts b/code/frameworks/angular/src/server/utils/normalize-asset-patterns.ts deleted file mode 100644 index 4d8d7279605b..000000000000 --- a/code/frameworks/angular/src/server/utils/normalize-asset-patterns.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Clone of `normalizeAssetPatterns` function from angular-cli v11.2.* - * > https://github.com/angular/angular-cli/blob/de63f41d669e42ada84f94ca1795d2791b9b45cc/packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts - * - * It is not possible to use the original because arguments have changed between version 6.1.* and 11.*.* of angular-cli - */ -import { statSync } from 'fs'; -import { - Path, - BaseException, - basename, - dirname, - getSystemPath, - join, - normalize, - relative, - resolve, -} from '@angular-devkit/core'; -import { AssetPattern } from '@angular-devkit/build-angular'; -import { AssetPatternClass } from '@angular-devkit/build-angular/src/builders/browser/schema'; - -export class MissingAssetSourceRootException extends BaseException { - constructor(path: string) { - super(`The ${path} asset path must start with the project source root.`); - } -} - -export function normalizeAssetPatterns( - assetPatterns: AssetPattern[], - root: Path, - projectRoot: Path, - maybeSourceRoot: Path | undefined -): AssetPatternClass[] { - // When sourceRoot is not available, we default to ${projectRoot}/src. - const sourceRoot = maybeSourceRoot || join(projectRoot, 'src'); - const resolvedSourceRoot = resolve(root, sourceRoot); - - if (assetPatterns.length === 0) { - return []; - } - - return assetPatterns.map((assetPattern) => { - // Normalize string asset patterns to objects. - if (typeof assetPattern === 'string') { - const assetPath = normalize(assetPattern); - const resolvedAssetPath = resolve(root, assetPath); - - // Check if the string asset is within sourceRoot. - if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) { - throw new MissingAssetSourceRootException(assetPattern); - } - - let glob: string; - let input: Path; - let isDirectory = false; - - try { - isDirectory = statSync(getSystemPath(resolvedAssetPath)).isDirectory(); - } catch { - isDirectory = true; - } - - if (isDirectory) { - // Folders get a recursive star glob. - glob = '**/*'; - // Input directory is their original path. - input = assetPath; - } else { - // Files are their own glob. - glob = basename(assetPath); - // Input directory is their original dirname. - input = dirname(assetPath); - } - - // Output directory for both is the relative path from source root to input. - const output = relative(resolvedSourceRoot, resolve(root, input)); - - // Return the asset pattern in object format. - return { glob, input, output }; - } - // It's already an AssetPatternObject, no need to convert. - return assetPattern; - }); -} diff --git a/code/frameworks/angular/src/server/utils/normalize-optimization.ts b/code/frameworks/angular/src/server/utils/normalize-optimization.ts deleted file mode 100644 index 345f3919e056..000000000000 --- a/code/frameworks/angular/src/server/utils/normalize-optimization.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { OptimizationUnion } from '@angular-devkit/build-angular'; -import { NormalizedOptimizationOptions } from '@angular-devkit/build-angular/src/utils/normalize-optimization'; -import { moduleIsAvailable } from './module-is-available'; - -const importAngularCliNormalizeOptimization = (): - | typeof import('@angular-devkit/build-angular/src/utils/normalize-optimization') - | undefined => { - // First we look for webpack config according to directory structure of Angular - // present since the version 7.2.0 - if (moduleIsAvailable('@angular-devkit/build-angular/src/utils/normalize-optimization')) { - // eslint-disable-next-line global-require - return require('@angular-devkit/build-angular/src/utils/normalize-optimization'); - } - return undefined; -}; - -export const normalizeOptimization = ( - options: OptimizationUnion -): NormalizedOptimizationOptions => { - if (importAngularCliNormalizeOptimization()) { - return importAngularCliNormalizeOptimization().normalizeOptimization(options); - } - - // Best effort to stay compatible with 6.1.* - return options as any; -}; diff --git a/code/frameworks/angular/template/cli/button.component.ts b/code/frameworks/angular/template/cli/button.component.ts index 3d0efd6af2f3..28dcc97e5526 100644 --- a/code/frameworks/angular/template/cli/button.component.ts +++ b/code/frameworks/angular/template/cli/button.component.ts @@ -1,7 +1,9 @@ +import { CommonModule } from '@angular/common'; import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'storybook-button', + imports: [CommonModule], template: `