diff --git a/docs/generated/packages/angular.json b/docs/generated/packages/angular.json index 03c3af81832b2..18fbdd14c93bd 100644 --- a/docs/generated/packages/angular.json +++ b/docs/generated/packages/angular.json @@ -2032,6 +2032,27 @@ "aliases": [], "hidden": false, "path": "/packages/angular/src/generators/web-worker/schema.json" + }, + { + "name": "change-storybook-targets", + "factory": "./src/generators/change-storybook-targets/change-storybook-targets", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxAngularChangeStorybookTargetsGenerator", + "title": "Change Storybook targets", + "description": "Change the Storybook target executors.", + "type": "object", + "cli": "nx", + "properties": {}, + "additionalProperties": false, + "required": [], + "presets": [] + }, + "description": "Change storybook targets for Angular projects to use @storybook/angular executors", + "implementation": "/packages/angular/src/generators/change-storybook-targets/change-storybook-targets.ts", + "aliases": [], + "hidden": false, + "path": "/packages/angular/src/generators/change-storybook-targets/schema.json" } ], "executors": [ diff --git a/docs/generated/packages/storybook.json b/docs/generated/packages/storybook.json index d167a320fed24..84aebf7f027b5 100644 --- a/docs/generated/packages/storybook.json +++ b/docs/generated/packages/storybook.json @@ -22,7 +22,7 @@ "id": "overview-angular", "name": "Overview (Angular)", "file": "shared/guides/storybook/plugin-angular", - "content": "# Storybook\n\n![Storybook logo](/shared/storybook-logo.png)\n\nStorybook is a development environment for UI components. It allows you to browse a component library, view the different states of each component, and interactively develop and test components.\n\nThis guide will briefly walk you through using Storybook within an Nx workspace.\n\n## Setting Up Storybook\n\n### Add the Storybook plugin\n\n```bash\nyarn add --dev @nrwl/storybook\n```\n\n## Using Storybook\n\n### Generating Storybook Configuration\n\nYou can generate Storybook configuration for an individual project with this command:\n\n```bash\nnx g @nrwl/angular:storybook-configuration project-name\n```\n\n### Running Storybook\n\nServe Storybook using this command:\n\n```bash\nnx run project-name:storybook\n```\n\n### Anatomy of the Storybook setup\n\nWhen running the Nx Storybook generator, it'll configure the Nx workspace to be able to run Storybook seamlessly. It'll create\n\n- a global Storybook configuration\n- a project specific Storybook configuration\n\nThe **global** Storybook configuration allows to set addon-ons or custom webpack configuration at a global level that applies to all Storybook's within the Nx workspace. You can find that folder at `.storybook/` at the root of the workspace.\n\n```treeview\n/\n├── .storybook/\n│ ├── main.js\n│ ├── tsconfig.json\n├── apps/\n├── libs/\n├── nx.json\n├── package.json\n├── README.md\n└── etc...\n```\n\nThe project-specific Storybook configuration is pretty much similar to what you would have for a non-Nx setup of Storybook. There's a `.storybook` folder within the project root folder.\n\n```treeview\n/\n├── .storybook/\n│ ├── main.js\n│ ├── preview.js\n│ ├── tsconfig.json\n├── src/\n├── README.md\n├── tsconfig.json\n└── etc...\n```\n\n### Using Addons\n\nTo register a [Storybook addon](https://storybook.js.org/addons/) for all storybook instances in your workspace:\n\n1. In `/.storybook/main.js`, in the `addons` array of the `module.exports` object, add the new addon:\n ```typescript\n module.exports = {\n stories: [...],\n ...,\n addons: [..., '@storybook/addon-essentials'],\n };\n ```\n2. If a decorator is required, in each project's `/.storybook/preview.js`, you can export an array called `decorators`.\n\n ```typescript\n import someDecorator from 'some-storybook-addon';\n export const decorators = [someDecorator];\n ```\n\n**-- OR --**\n\nTo register an [addon](https://storybook.js.org/addons/) for a single storybook instance, go to that project's `.storybook` folder:\n\n1. In `main.js`, in the `addons` array of the `module.exports` object, add the new addon:\n ```typescript\n module.exports = {\n stories: [...],\n ...,\n addons: [..., '@storybook/addon-essentials'],\n };\n ```\n2. If a decorator is required, in `preview.js` you can export an array called `decorators`.\n\n ```typescript\n import someDecorator from 'some-storybook-addon';\n export const decorators = [someDecorator];\n ```\n\n### Auto-generate Stories\n\nThe `@nrwl/angular:storybook-configuration` generator has the option to automatically generate `*.stories.ts` files for each component declared in the library.\n\n```treeview\n/\n├── my.component.ts\n└── my.component.stories.ts\n```\n\nYou can re-run it at a later point using the following command:\n\n```bash\nnx g @nrwl/angular:stories \n```\n\n### Cypress tests for Stories\n\nBoth `storybook-configuration` generator gives the option to set up an e2e Cypress app that is configured to run against the project's Storybook instance.\n\nTo launch Storybook and run the Cypress tests against the iframe inside of Storybook:\n\n```bash\nnx run project-name-e2e:e2e\n```\n\nThe url that Cypress points to should look like this:\n\n`'/iframe.html?id=buttoncomponent--primary&args=text:Click+me!;padding;style:default'`\n\n- `buttoncomponent` is a lowercase version of the `Title` in the `*.stories.ts` file.\n- `primary` is the name of an individual story.\n- `style=default` sets the `style` arg to a value of `default`.\n\nChanging args in the url query parameters allows your Cypress tests to test different configurations of your component. You can [read the documentation](https://storybook.js.org/docs/angular/writing-stories/args#setting-args-through-the-url) for more information.\n\n### Example Files\n\n**\\*.component.stories.ts file**\n\n```typescript\nimport { moduleMetadata, Story, Meta } from '@storybook/angular';\nimport { ButtonComponent } from './button.component';\n\nexport default {\n title: 'ButtonComponent',\n component: ButtonComponent,\n decorators: [\n moduleMetadata({\n imports: [],\n }),\n ],\n} as Meta;\n\nconst Template: Story = (args: ButtonComponent) => ({\n props: args,\n});\n\nexport const Primary = Template.bind({});\nPrimary.args = {\n text: 'Click me!',\n padding: 0,\n style: 'default',\n};\n```\n\n**Cypress \\*.spec.ts file**\n\n```typescript\ndescribe('shared-ui', () => {\n beforeEach(() =>\n cy.visit(\n '/iframe.html?id=buttoncomponent--primary&args=text:Click+me!;padding;style:default'\n )\n );\n\n it('should render the component', () => {\n cy.get('storybook-trial-button').should('exist');\n });\n});\n```\n\n### Setting up `projectBuildConfig`\n\nStorybook for Angular needs a default project specified in order to run. The reason is that it uses that default project to read the build configuration from (paths to files to include in the build, and other configurations/settings). In Nx workspaces, that project is specified with the `projectBuildConfig` property.\n\nIf you're using Nx version `>=13.4.6` either in a new Nx workspace, or you migrated your older Nx workspace to Nx version `>=13.4.6`, Nx will automatically add the `projectBuildConfig` property in your projects `project.json` files, for projects that are using Storybook. It will look like this:\n\n```json\n \"storybook\": {\n \"executor\": \"@nrwl/storybook:storybook\",\n \"options\": {\n ...\n \"projectBuildConfig\": \"my-project:build-storybook\"\n },\n ...\n },\n \"build-storybook\": {\n \"executor\": \"@nrwl/storybook:build\",\n ...\n \"options\": {\n ...\n \"projectBuildConfig\": \"my-project:build-storybook\"\n },\n ...\n }\n```\n\nThis setup instructs Nx to use the configuration under the `build-storybook` target of `my-project` when using the `storybook` and `build-storybook` executors.\n\nIf the `projectBuildConfig` is not set in your `project.json`, you can manually set it up in one of the following ways:\n\n#### Adding the `projectBuildConfig` option directly in the project's `project.json`\n\nIn your project's `project.json` file find the `storybook` and `build-storybook` targets. Add the `projectBuildConfig` property under the `options` as shown above.\n\nAfter you add this property, you can run your `storybook` and `build-storybook` executors as normal:\n\n```bash\nnx storybook my-project\n```\n\nand\n\n```bash\nnx build-storybook my-project\n```\n\n#### Using the `projectBuildConfig` flag on the executors\n\nThe way you would run your `storybook` and your `build-storybook` executors would be:\n\n```bash\nnx storybook my-project --projectBuildConfig=my-project:build-storybook\n```\n\nand\n\n```bash\nnx build-storybook my-project --projectBuildConfig=my-project:build-storybook\n```\n\n**Note:** If your project is buildable (eg. any project that has a `build` target set up in its `project.json`) you can also do `nx storybook my-project --projectBuildConfig=my-project`.\n\n> In a pure Angular/Storybook setup (**not** an Nx workspace), the Angular application/project would have an `angular.json` file. That file would have a property called `defaultProject`. In an Nx workspace the `defaultProject` property would be specified in the `nx.json` file. Previously, Nx would try to resolve the `defaultProject` of the workspace, and use the build configuration of that project. In most cases, the `defaultProject`'s build configuration would not work for some other project set up with Storybook, since there would most probably be mismatches in paths or other project-specific options.\n\n### Configuring styles and preprocessor options\n\nAngular supports including extra entry-point files for styles. Also, in case you use Sass, you can add extra base paths that will be checked for imports. In your project's `project.json` file you can use the `styles` and `stylePreprocessorOptions` properties in your `storybook` and `build-storybook` target `options`, as you would in your Storybook or your Angular configurations. Check out the [Angular Workspace Configuration](https://angular.io/guide/workspace-config#styles-and-scripts-configuration) documentation for more information.\n\n```json\n \"storybook\": {\n \"executor\": \"@nrwl/storybook:storybook\",\n \"options\": {\n ...\n \"styles\": [\"some-styles.css\"],\n \"stylePreprocessorOptions\": {\n \"includePaths\": [\"some-style-paths\"]\n }\n },\n ...\n },\n \"build-storybook\": {\n \"executor\": \"@nrwl/storybook:build\",\n ...\n \"options\": {\n ...\n \"styles\": [\"some-styles.css\"],\n \"stylePreprocessorOptions\": {\n \"includePaths\": [\"some-style-paths\"]\n }\n },\n ...\n }\n```\n\n## More Documentation\n\nFor more on using Storybook, see the [official Storybook documentation](https://storybook.js.org/docs/basics/introduction/).\n\n### Migration Scenarios\n\nHere's more information on common migration scenarios for Storybook with Nx. For Storybook specific migrations that are not automatically handled by Nx please refer to the [official Storybook page](https://storybook.js.org/)\n\n- [Upgrading to Storybook 6](/storybook/upgrade-storybook-v6-angular)\n- [Migrate to the new Storybook `webpackFinal` config](/storybook/migrate-webpack-final-angular)\n" + "content": "# Storybook\n\n![Storybook logo](/shared/storybook-logo.png)\n\nStorybook is a development environment for UI components. It allows you to browse a component library, view the different states of each component, and interactively develop and test components.\n\nThis guide will briefly walk you through using Storybook within an Nx workspace.\n\n## Setting Up Storybook\n\n### Add the Storybook plugin\n\n```bash\nyarn add --dev @nrwl/storybook\n```\n\n## Using Storybook\n\n### Generating Storybook Configuration\n\nYou can generate Storybook configuration for an individual project with this command:\n\n```bash\nnx g @nrwl/angular:storybook-configuration project-name\n```\n\n### Running Storybook\n\nServe Storybook using this command:\n\n```bash\nnx run project-name:storybook\n```\n\n### Anatomy of the Storybook setup\n\nWhen running the Nx Storybook generator, it'll configure the Nx workspace to be able to run Storybook seamlessly. It'll create\n\n- a global Storybook configuration\n- a project specific Storybook configuration\n\nThe **global** Storybook configuration allows to set addon-ons or custom webpack configuration at a global level that applies to all Storybook's within the Nx workspace. You can find that folder at `.storybook/` at the root of the workspace.\n\n```treeview\n/\n├── .storybook/\n│ ├── main.js\n│ ├── tsconfig.json\n├── apps/\n├── libs/\n├── nx.json\n├── package.json\n├── README.md\n└── etc...\n```\n\nThe project-specific Storybook configuration is pretty much similar to what you would have for a non-Nx setup of Storybook. There's a `.storybook` folder within the project root folder.\n\n```treeview\n/\n├── .storybook/\n│ ├── main.js\n│ ├── preview.js\n│ ├── tsconfig.json\n├── src/\n├── README.md\n├── tsconfig.json\n└── etc...\n```\n\n### Using Addons\n\nTo register a [Storybook addon](https://storybook.js.org/addons/) for all storybook instances in your workspace:\n\n1. In `/.storybook/main.js`, in the `addons` array of the `module.exports` object, add the new addon:\n ```typescript\n module.exports = {\n stories: [...],\n ...,\n addons: [..., '@storybook/addon-essentials'],\n };\n ```\n2. If a decorator is required, in each project's `/.storybook/preview.js`, you can export an array called `decorators`.\n\n ```typescript\n import someDecorator from 'some-storybook-addon';\n export const decorators = [someDecorator];\n ```\n\n**-- OR --**\n\nTo register an [addon](https://storybook.js.org/addons/) for a single storybook instance, go to that project's `.storybook` folder:\n\n1. In `main.js`, in the `addons` array of the `module.exports` object, add the new addon:\n ```typescript\n module.exports = {\n stories: [...],\n ...,\n addons: [..., '@storybook/addon-essentials'],\n };\n ```\n2. If a decorator is required, in `preview.js` you can export an array called `decorators`.\n\n ```typescript\n import someDecorator from 'some-storybook-addon';\n export const decorators = [someDecorator];\n ```\n\n### Auto-generate Stories\n\nThe `@nrwl/angular:storybook-configuration` generator has the option to automatically generate `*.stories.ts` files for each component declared in the library.\n\n```treeview\n/\n├── my.component.ts\n└── my.component.stories.ts\n```\n\nYou can re-run it at a later point using the following command:\n\n```bash\nnx g @nrwl/angular:stories \n```\n\n### Cypress tests for Stories\n\nBoth `storybook-configuration` generator gives the option to set up an e2e Cypress app that is configured to run against the project's Storybook instance.\n\nTo launch Storybook and run the Cypress tests against the iframe inside of Storybook:\n\n```bash\nnx run project-name-e2e:e2e\n```\n\nThe url that Cypress points to should look like this:\n\n`'/iframe.html?id=buttoncomponent--primary&args=text:Click+me!;padding;style:default'`\n\n- `buttoncomponent` is a lowercase version of the `Title` in the `*.stories.ts` file.\n- `primary` is the name of an individual story.\n- `style=default` sets the `style` arg to a value of `default`.\n\nChanging args in the url query parameters allows your Cypress tests to test different configurations of your component. You can [read the documentation](https://storybook.js.org/docs/angular/writing-stories/args#setting-args-through-the-url) for more information.\n\n### Example Files\n\n**\\*.component.stories.ts file**\n\n```typescript\nimport { moduleMetadata, Story, Meta } from '@storybook/angular';\nimport { ButtonComponent } from './button.component';\n\nexport default {\n title: 'ButtonComponent',\n component: ButtonComponent,\n decorators: [\n moduleMetadata({\n imports: [],\n }),\n ],\n} as Meta;\n\nconst Template: Story = (args: ButtonComponent) => ({\n props: args,\n});\n\nexport const Primary = Template.bind({});\nPrimary.args = {\n text: 'Click me!',\n padding: 0,\n style: 'default',\n};\n```\n\n**Cypress \\*.spec.ts file**\n\n```typescript\ndescribe('shared-ui', () => {\n beforeEach(() =>\n cy.visit(\n '/iframe.html?id=buttoncomponent--primary&args=text:Click+me!;padding;style:default'\n )\n );\n\n it('should render the component', () => {\n cy.get('storybook-trial-button').should('exist');\n });\n});\n```\n\n### Storybook uses `browserTarget` for Angular\n\nNx is using the original Storybook executor for Angular to serve and build Storybook. If you're using Storybook in\nyour Angular project, you will notice that `browserTarget` is specified for the `storybook` and `build-storybook` targets, much like it is done for `serve` or other targets. Angular needs the `browserTarget` for Storybook in order to know which configuration to use for the build. If your project is buildable (it has a `build` target, and uses the main Angular builder - `@angular-devkit/build-angular:browser`) the `browserTarget` for Storybook will use the `build` target, if it's not buildable it will use the `build-storybook` target.\n\n```json\n \"storybook\": {\n \"executor\": \"@storybook/angular:start-storybook\",\n \"options\": {\n ...\n \"browserTarget\": \"my-project:build\"\n },\n ...\n },\n \"build-storybook\": {\n \"executor\": \"@storybook/angular:build-storybook\",\n ...\n \"options\": {\n ...\n \"browserTarget\": \"my-project:build\"\n },\n ...\n }\n```\n\nThis setup instructs Nx to use the configuration under the `build` target of `my-project` when using the `storybook` and `build-storybook` executors.\n\n### Configuring styles and preprocessor options\n\nAngular supports including extra entry-point files for styles. Also, in case you use Sass, you can add extra base paths that will be checked for imports. In your project's `project.json` file you can use the `styles` and `stylePreprocessorOptions` properties in your `storybook` and `build-storybook` target `options`, as you would in your Storybook or your Angular configurations. Check out the [Angular Workspace Configuration](https://angular.io/guide/workspace-config#styles-and-scripts-configuration) documentation for more information.\n\n```json\n \"storybook\": {\n \"executor\": \"@storybook/angular:start-storybook\",\n \"options\": {\n ...\n \"styles\": [\"some-styles.css\"],\n \"stylePreprocessorOptions\": {\n \"includePaths\": [\"some-style-paths\"]\n }\n },\n ...\n },\n \"build-storybook\": {\n \"executor\": \"@storybook/angular:build-storybook\",\n ...\n \"options\": {\n ...\n \"styles\": [\"some-styles.css\"],\n \"stylePreprocessorOptions\": {\n \"includePaths\": [\"some-style-paths\"]\n }\n },\n ...\n }\n```\n\n> **Note**: Chances are, you will most probably need the same `styles` and `stylePreprocessorOptions` for your `storybook` and your `build-storybook` targets. Since you're using `browserTarget`, that means that Storybook will use the `options` of `build` or `build-storybook` when executing the `storybook` task (when compiling your Storybook). In that case, you _only_ need to add the `styles` or `stylePreprocessorOptions` to the corresponding target (`build` or `build-storybook`) that the `browserTarget` is pointing to. In that case, for example, the configuration shown above would look like this:\n\n```json\n \"storybook\": {\n \"executor\": \"@storybook/angular:start-storybook\",\n \"options\": {\n ...\n \"browserTarget\": \"my-project:build-storybook\"\n },\n ...\n },\n \"build-storybook\": {\n \"executor\": \"@storybook/angular:build-storybook\",\n ...\n \"options\": {\n ...\n \"browserTarget\": \"my-project:build-storybook\",\n \"styles\": [\"some-styles.css\"],\n \"stylePreprocessorOptions\": {\n \"includePaths\": [\"some-style-paths\"]\n }\n },\n ...\n }\n```\n\n## More Documentation\n\nFor more on using Storybook, see the [official Storybook documentation](https://storybook.js.org/docs/basics/introduction/).\n\n### Migration Scenarios\n\nHere's more information on common migration scenarios for Storybook with Nx. For Storybook specific migrations that are not automatically handled by Nx please refer to the [official Storybook page](https://storybook.js.org/)\n\n- [Upgrading to Storybook 6](/storybook/upgrade-storybook-v6-angular)\n- [Migrate to the new Storybook `webpackFinal` config](/storybook/migrate-webpack-final-angular)\n" }, { "id": "migrate-webpack-final-react", @@ -194,6 +194,26 @@ "implementation": "/packages/storybook/src/generators/cypress-project/cypress-project.ts", "aliases": [], "path": "/packages/storybook/src/generators/cypress-project/schema.json" + }, + { + "name": "change-storybook-targets", + "factory": "./src/generators/change-storybook-targets/change-storybook-targets", + "schema": { + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "change-storybook-targets", + "title": "Change Storybook targets", + "description": "Change the Storybook target executors.", + "type": "object", + "properties": {}, + "required": [], + "presets": [] + }, + "description": "Change storybook targets for Angular projects to use @storybook/angular executors", + "hidden": false, + "implementation": "/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.ts", + "aliases": [], + "path": "/packages/storybook/src/generators/change-storybook-targets/schema.json" } ], "executors": [ @@ -227,7 +247,6 @@ "type": "string", "description": "Storybook framework npm package.", "enum": [ - "@storybook/angular", "@storybook/react", "@storybook/html", "@storybook/web-components", @@ -276,47 +295,6 @@ "type": "string", "description": "Workspace project where Storybook reads the Webpack config from." }, - "styles": { - "type": "array", - "description": "Global styles to be included in the build. This is for Angular projects only. It will be ignored in non-Angular projects.", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "The file to include." - }, - "bundleName": { - "type": "string", - "pattern": "^[\\w\\-.]*$", - "description": "The bundle name for this extra entry point." - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true - } - }, - "additionalProperties": false, - "required": ["input"] - }, - { "type": "string", "description": "The file to include." } - ] - } - }, - "stylePreprocessorOptions": { - "type": "object", - "description": "Options to pass to style preprocessors.", - "properties": { - "includePaths": { - "type": "array", - "description": "The paths to include. Paths will be resolved to workspace root. This is for Angular projects only. It will be ignored in non-Angular projects.", - "items": { "type": "string" } - } - } - }, "config": { "type": "object", "description": ".storybook configuration.", @@ -350,34 +328,7 @@ "default": true } }, - "definitions": { - "extraEntryPoint": { - "oneOf": [ - { - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "The file to include." - }, - "bundleName": { - "type": "string", - "pattern": "^[\\w\\-.]*$", - "description": "The bundle name for this extra entry point." - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true - } - }, - "additionalProperties": false, - "required": ["input"] - }, - { "type": "string", "description": "The file to include." } - ] - } - }, + "definitions": {}, "required": ["uiFramework", "config"] }, "description": "Serve Storybook.", diff --git a/docs/packages.json b/docs/packages.json index 4b3d7d287b7b8..148d4bc9eef01 100644 --- a/docs/packages.json +++ b/docs/packages.json @@ -44,7 +44,8 @@ "stories", "storybook-configuration", "upgrade-module", - "web-worker" + "web-worker", + "change-storybook-targets" ] } }, @@ -243,7 +244,12 @@ "path": "generated/packages/storybook.json", "schemas": { "executors": ["storybook", "build"], - "generators": ["init", "configuration", "cypress-project"] + "generators": [ + "init", + "configuration", + "cypress-project", + "change-storybook-targets" + ] } }, { diff --git a/docs/shared/guides/storybook/plugin-angular.md b/docs/shared/guides/storybook/plugin-angular.md index 2470f6d9c5ca7..6d388bc1c8ec7 100644 --- a/docs/shared/guides/storybook/plugin-angular.md +++ b/docs/shared/guides/storybook/plugin-angular.md @@ -188,69 +188,32 @@ describe('shared-ui', () => { }); ``` -### Setting up `projectBuildConfig` +### Storybook uses `browserTarget` for Angular -Storybook for Angular needs a default project specified in order to run. The reason is that it uses that default project to read the build configuration from (paths to files to include in the build, and other configurations/settings). In Nx workspaces, that project is specified with the `projectBuildConfig` property. - -If you're using Nx version `>=13.4.6` either in a new Nx workspace, or you migrated your older Nx workspace to Nx version `>=13.4.6`, Nx will automatically add the `projectBuildConfig` property in your projects `project.json` files, for projects that are using Storybook. It will look like this: +Nx is using the original Storybook executor for Angular to serve and build Storybook. If you're using Storybook in +your Angular project, you will notice that `browserTarget` is specified for the `storybook` and `build-storybook` targets, much like it is done for `serve` or other targets. Angular needs the `browserTarget` for Storybook in order to know which configuration to use for the build. If your project is buildable (it has a `build` target, and uses the main Angular builder - `@angular-devkit/build-angular:browser`) the `browserTarget` for Storybook will use the `build` target, if it's not buildable it will use the `build-storybook` target. ```json "storybook": { - "executor": "@nrwl/storybook:storybook", + "executor": "@storybook/angular:start-storybook", "options": { ... - "projectBuildConfig": "my-project:build-storybook" + "browserTarget": "my-project:build" }, ... }, "build-storybook": { - "executor": "@nrwl/storybook:build", + "executor": "@storybook/angular:build-storybook", ... "options": { ... - "projectBuildConfig": "my-project:build-storybook" + "browserTarget": "my-project:build" }, ... } ``` -This setup instructs Nx to use the configuration under the `build-storybook` target of `my-project` when using the `storybook` and `build-storybook` executors. - -If the `projectBuildConfig` is not set in your `project.json`, you can manually set it up in one of the following ways: - -#### Adding the `projectBuildConfig` option directly in the project's `project.json` - -In your project's `project.json` file find the `storybook` and `build-storybook` targets. Add the `projectBuildConfig` property under the `options` as shown above. - -After you add this property, you can run your `storybook` and `build-storybook` executors as normal: - -```bash -nx storybook my-project -``` - -and - -```bash -nx build-storybook my-project -``` - -#### Using the `projectBuildConfig` flag on the executors - -The way you would run your `storybook` and your `build-storybook` executors would be: - -```bash -nx storybook my-project --projectBuildConfig=my-project:build-storybook -``` - -and - -```bash -nx build-storybook my-project --projectBuildConfig=my-project:build-storybook -``` - -**Note:** If your project is buildable (eg. any project that has a `build` target set up in its `project.json`) you can also do `nx storybook my-project --projectBuildConfig=my-project`. - -> In a pure Angular/Storybook setup (**not** an Nx workspace), the Angular application/project would have an `angular.json` file. That file would have a property called `defaultProject`. In an Nx workspace the `defaultProject` property would be specified in the `nx.json` file. Previously, Nx would try to resolve the `defaultProject` of the workspace, and use the build configuration of that project. In most cases, the `defaultProject`'s build configuration would not work for some other project set up with Storybook, since there would most probably be mismatches in paths or other project-specific options. +This setup instructs Nx to use the configuration under the `build` target of `my-project` when using the `storybook` and `build-storybook` executors. ### Configuring styles and preprocessor options @@ -258,7 +221,19 @@ Angular supports including extra entry-point files for styles. Also, in case you ```json "storybook": { - "executor": "@nrwl/storybook:storybook", + "executor": "@storybook/angular:start-storybook", + "options": { + ... + "styles": ["some-styles.css"], + "stylePreprocessorOptions": { + "includePaths": ["some-style-paths"] + } + }, + ... + }, + "build-storybook": { + "executor": "@storybook/angular:build-storybook", + ... "options": { ... "styles": ["some-styles.css"], @@ -266,13 +241,27 @@ Angular supports including extra entry-point files for styles. Also, in case you "includePaths": ["some-style-paths"] } }, + ... + } +``` + +> **Note**: Chances are, you will most probably need the same `styles` and `stylePreprocessorOptions` for your `storybook` and your `build-storybook` targets. Since you're using `browserTarget`, that means that Storybook will use the `options` of `build` or `build-storybook` when executing the `storybook` task (when compiling your Storybook). In that case, you _only_ need to add the `styles` or `stylePreprocessorOptions` to the corresponding target (`build` or `build-storybook`) that the `browserTarget` is pointing to. In that case, for example, the configuration shown above would look like this: + +```json + "storybook": { + "executor": "@storybook/angular:start-storybook", + "options": { + ... + "browserTarget": "my-project:build-storybook" + }, ... }, "build-storybook": { - "executor": "@nrwl/storybook:build", + "executor": "@storybook/angular:build-storybook", ... "options": { ... + "browserTarget": "my-project:build-storybook", "styles": ["some-styles.css"], "stylePreprocessorOptions": { "includePaths": ["some-style-paths"] diff --git a/packages/angular/generators.json b/packages/angular/generators.json index 8b170fcf06922..de25510d2fa2d 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -148,6 +148,12 @@ "factory": "./src/generators/web-worker/compat", "schema": "./src/generators/web-worker/schema.json", "description": "Creates a Web Worker." + }, + "change-storybook-targets": { + "factory": "./src/generators/change-storybook-targets/compat", + "schema": "./src/generators/change-storybook-targets/schema.json", + "description": "Change storybook targets for Angular projects to use @storybook/angular executors", + "hidden": false } }, "generators": { @@ -298,6 +304,11 @@ "factory": "./src/generators/web-worker/web-worker", "schema": "./src/generators/web-worker/schema.json", "description": "Creates a Web Worker." + }, + "change-storybook-targets": { + "factory": "./src/generators/change-storybook-targets/change-storybook-targets", + "schema": "./src/generators/change-storybook-targets/schema.json", + "description": "Change storybook targets for Angular projects to use @storybook/angular executors" } } } diff --git a/packages/angular/generators.ts b/packages/angular/generators.ts index d1bd4ef8dae27..3f3d22f5872d2 100644 --- a/packages/angular/generators.ts +++ b/packages/angular/generators.ts @@ -22,3 +22,4 @@ export * from './src/generators/component-cypress-spec/component-cypress-spec'; export * from './src/generators/component-story/component-story'; export * from './src/generators/web-worker/web-worker'; export * from './src/generators/remote/remote'; +export * from './src/generators/change-storybook-targets/change-storybook-targets'; diff --git a/packages/angular/src/generators/change-storybook-targets/change-storybook-targets.ts b/packages/angular/src/generators/change-storybook-targets/change-storybook-targets.ts new file mode 100644 index 0000000000000..a99e04ad65184 --- /dev/null +++ b/packages/angular/src/generators/change-storybook-targets/change-storybook-targets.ts @@ -0,0 +1,8 @@ +import type { Tree } from '@nrwl/devkit'; +import { changeStorybookTargetsGenerator } from '@nrwl/storybook'; + +export async function angularChangeStorybookTargestGenerator(tree: Tree) { + await changeStorybookTargetsGenerator(tree); +} + +export default angularChangeStorybookTargestGenerator; diff --git a/packages/angular/src/generators/change-storybook-targets/compat.ts b/packages/angular/src/generators/change-storybook-targets/compat.ts new file mode 100644 index 0000000000000..b00ad49789078 --- /dev/null +++ b/packages/angular/src/generators/change-storybook-targets/compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nrwl/devkit'; +import angularChangeStorybookTargestGenerator from './change-storybook-targets'; + +export default convertNxGenerator(angularChangeStorybookTargestGenerator); diff --git a/packages/angular/src/generators/change-storybook-targets/schema.json b/packages/angular/src/generators/change-storybook-targets/schema.json new file mode 100644 index 0000000000000..8201fb1a7a177 --- /dev/null +++ b/packages/angular/src/generators/change-storybook-targets/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxAngularChangeStorybookTargetsGenerator", + "title": "Change Storybook targets", + "description": "Change the Storybook target executors.", + "type": "object", + "cli": "nx", + "properties": {}, + "additionalProperties": false, + "required": [] +} diff --git a/packages/storybook/generators.json b/packages/storybook/generators.json index bec4d21384593..93350a3cb3f2d 100644 --- a/packages/storybook/generators.json +++ b/packages/storybook/generators.json @@ -20,6 +20,12 @@ "schema": "./src/generators/cypress-project/schema.json", "description": "Add cypress e2e app to test a UI library that is set up for Storybook.", "hidden": false + }, + "change-storybook-targets": { + "factory": "./src/generators/change-storybook-targets/change-storybook-targets#changeStorybookTargetsSchematic", + "schema": "./src/generators/change-storybook-targets/schema.json", + "description": "Change storybook targets for Angular projects to use @storybook/angular executors", + "hidden": false } }, "generators": { @@ -41,6 +47,12 @@ "schema": "./src/generators/cypress-project/schema.json", "description": "Add cypress e2e app to test a UI library that is set up for Storybook.", "hidden": false + }, + "change-storybook-targets": { + "factory": "./src/generators/change-storybook-targets/change-storybook-targets", + "schema": "./src/generators/change-storybook-targets/schema.json", + "description": "Change storybook targets for Angular projects to use @storybook/angular executors", + "hidden": false } } } diff --git a/packages/storybook/index.ts b/packages/storybook/index.ts index 731af7dde6584..99767d862ee9e 100644 --- a/packages/storybook/index.ts +++ b/packages/storybook/index.ts @@ -1,3 +1,4 @@ export { configurationGenerator } from './src/generators/configuration/configuration'; export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project'; +export { changeStorybookTargetsGenerator } from './src/generators/change-storybook-targets/change-storybook-targets'; export { storybookVersion } from './src/utils/versions'; diff --git a/packages/storybook/migrations.json b/packages/storybook/migrations.json index d9189472e4790..9ea0a2fb9ed5d 100644 --- a/packages/storybook/migrations.json +++ b/packages/storybook/migrations.json @@ -84,6 +84,12 @@ "cli": "nx", "description": "Migrate Storybook to v6", "factory": "./src/migrations/update-14-0-0/migrate-to-storybook-6" + }, + "update-14.1.8": { + "version": "14.1.8", + "cli": "nx", + "description": "Change storybook targets for Angular projects to use @storybook/angular executors", + "factory": "./src/migrations/update-14-1-8/change-storybook-targets" } }, "packageJsonUpdates": { diff --git a/packages/storybook/src/executors/build-storybook/build-storybook.impl.spec.ts b/packages/storybook/src/executors/build-storybook/build-storybook.impl.spec.ts index 031f189bc5035..8ee8a5d77b8b1 100644 --- a/packages/storybook/src/executors/build-storybook/build-storybook.impl.spec.ts +++ b/packages/storybook/src/executors/build-storybook/build-storybook.impl.spec.ts @@ -1,5 +1,4 @@ import { ExecutorContext, logger } from '@nrwl/devkit'; - import { join } from 'path'; jest.mock('@storybook/core/standalone', () => jest.fn().mockImplementation(() => Promise.resolve()) @@ -17,7 +16,7 @@ describe('Build storybook', () => { let config: StorybookBuilderOptions['config']; beforeEach(async () => { - uiFramework = '@storybook/angular'; + uiFramework = '@storybook/react'; outputPath = '/root/dist/storybook'; config = { pluginPath: join( @@ -36,11 +35,6 @@ describe('Build storybook', () => { options = { uiFramework, outputPath, - projectBuildConfig: 'proj', - stylePreprocessorOptions: { - includePaths: ['my-path/my-style-options'], - }, - styles: ['my-other-path/my-other-styles.scss'], config, }; @@ -57,12 +51,22 @@ describe('Build storybook', () => { sourceRoot: 'src', targets: { build: { - executor: '@angular-devkit/build-angular:browser', + executor: '@nrwl/web:webpack', options: { - main: 'apps/proj/src/main.ts', - outputPath: 'dist/apps/proj', - tsConfig: 'apps/proj/tsconfig.app.json', - index: 'apps/proj/src/index.html', + compiler: 'babel', + outputPath: 'dist/apps/webre', + index: 'apps/webre/src/index.html', + baseHref: '/', + main: 'apps/webre/src/main.tsx', + polyfills: 'apps/webre/src/polyfills.ts', + tsConfig: 'apps/webre/tsconfig.app.json', + assets: [ + 'apps/webre/src/favicon.ico', + 'apps/webre/src/assets', + ], + styles: ['apps/webre/src/styles.css'], + scripts: [], + webpackConfig: '@nrwl/react/plugins/webpack', }, }, storybook: { @@ -72,7 +76,6 @@ describe('Build storybook', () => { }, }, }, - defaultProject: 'proj', npmScope: 'test', }, isVerbose: false, diff --git a/packages/storybook/src/executors/build-storybook/build-storybook.impl.ts b/packages/storybook/src/executors/build-storybook/build-storybook.impl.ts index 75ac0c221bdee..cb6fe99511487 100644 --- a/packages/storybook/src/executors/build-storybook/build-storybook.impl.ts +++ b/packages/storybook/src/executors/build-storybook/build-storybook.impl.ts @@ -4,7 +4,6 @@ import 'dotenv/config'; import { CommonNxStorybookConfig } from '../models'; import { getStorybookFrameworkPath, - normalizeAngularBuilderStylesOptions, resolveCommonStorybookOptionMapper, runStorybookSetupCheck, } from '../utils'; @@ -24,7 +23,6 @@ export default async function buildStorybookExecutor( const frameworkPath = getStorybookFrameworkPath(options.uiFramework); const { default: frameworkOptions } = await import(frameworkPath); - options = normalizeAngularBuilderStylesOptions(options, options.uiFramework); const option = storybookOptionMapper(options, frameworkOptions, context); // print warnings diff --git a/packages/storybook/src/executors/models.ts b/packages/storybook/src/executors/models.ts index b695d8a759418..b3b7c8de69d86 100644 --- a/packages/storybook/src/executors/models.ts +++ b/packages/storybook/src/executors/models.ts @@ -16,9 +16,5 @@ export interface CommonNxStorybookConfig { | '@storybook/svelte' | '@storybook/react-native'; projectBuildConfig?: string; - styles?: any[]; - stylePreprocessorOptions?: { - includePaths?: string[]; - }; config: StorybookConfig; } diff --git a/packages/storybook/src/executors/storybook/schema.json b/packages/storybook/src/executors/storybook/schema.json index faf8419d4f056..53274612343fb 100644 --- a/packages/storybook/src/executors/storybook/schema.json +++ b/packages/storybook/src/executors/storybook/schema.json @@ -25,7 +25,6 @@ "type": "string", "description": "Storybook framework npm package.", "enum": [ - "@storybook/angular", "@storybook/react", "@storybook/html", "@storybook/web-components", @@ -76,26 +75,6 @@ "type": "string", "description": "Workspace project where Storybook reads the Webpack config from." }, - "styles": { - "type": "array", - "description": "Global styles to be included in the build. This is for Angular projects only. It will be ignored in non-Angular projects.", - "items": { - "$ref": "#/definitions/extraEntryPoint" - } - }, - "stylePreprocessorOptions": { - "type": "object", - "description": "Options to pass to style preprocessors.", - "properties": { - "includePaths": { - "type": "array", - "description": "The paths to include. Paths will be resolved to workspace root. This is for Angular projects only. It will be ignored in non-Angular projects.", - "items": { - "type": "string" - } - } - } - }, "config": { "type": "object", "description": ".storybook configuration.", @@ -129,36 +108,6 @@ "default": true } }, - "definitions": { - "extraEntryPoint": { - "oneOf": [ - { - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "The file to include." - }, - "bundleName": { - "type": "string", - "pattern": "^[\\w\\-.]*$", - "description": "The bundle name for this extra entry point." - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true - } - }, - "additionalProperties": false, - "required": ["input"] - }, - { - "type": "string", - "description": "The file to include." - } - ] - } - }, + "definitions": {}, "required": ["uiFramework", "config"] } diff --git a/packages/storybook/src/executors/storybook/storybook.impl.spec.ts b/packages/storybook/src/executors/storybook/storybook.impl.spec.ts index dd8f6fc127762..894131aba9c2b 100644 --- a/packages/storybook/src/executors/storybook/storybook.impl.spec.ts +++ b/packages/storybook/src/executors/storybook/storybook.impl.spec.ts @@ -20,14 +20,13 @@ describe('@nrwl/storybook:storybook', () => { const rootPath = join(__dirname, `../../../../../`); const packageJsonPath = join( rootPath, - `node_modules/@storybook/angular/package.json` + `node_modules/@storybook/react/package.json` ); const storybookPath = join(rootPath, '.storybook'); options = { - uiFramework: '@storybook/angular', + uiFramework: '@storybook/react', port: 4400, - projectBuildConfig: 'proj', config: { configFolder: storybookPath, }, @@ -51,12 +50,22 @@ describe('@nrwl/storybook:storybook', () => { sourceRoot: 'src', targets: { build: { - executor: '@angular-devkit/build-angular:browser', + executor: '@nrwl/web:webpack', options: { - main: 'apps/proj/src/main.ts', - outputPath: 'dist/apps/proj', - tsConfig: 'apps/proj/tsconfig.app.json', - index: 'apps/proj/src/index.html', + compiler: 'babel', + outputPath: 'dist/apps/webre', + index: 'apps/webre/src/index.html', + baseHref: '/', + main: 'apps/webre/src/main.tsx', + polyfills: 'apps/webre/src/polyfills.ts', + tsConfig: 'apps/webre/tsconfig.app.json', + assets: [ + 'apps/webre/src/favicon.ico', + 'apps/webre/src/assets', + ], + styles: ['apps/webre/src/styles.css'], + scripts: [], + webpackConfig: '@nrwl/react/plugins/webpack', }, }, storybook: { @@ -66,7 +75,6 @@ describe('@nrwl/storybook:storybook', () => { }, }, }, - defaultProject: 'proj', npmScope: 'test', }, isVerbose: false, diff --git a/packages/storybook/src/executors/storybook/storybook.impl.ts b/packages/storybook/src/executors/storybook/storybook.impl.ts index b417118623ee6..405b89942e1b7 100644 --- a/packages/storybook/src/executors/storybook/storybook.impl.ts +++ b/packages/storybook/src/executors/storybook/storybook.impl.ts @@ -4,7 +4,6 @@ import 'dotenv/config'; import { CommonNxStorybookConfig } from '../models'; import { getStorybookFrameworkPath, - normalizeAngularBuilderStylesOptions, resolveCommonStorybookOptionMapper, runStorybookSetupCheck, } from '../utils'; @@ -27,7 +26,6 @@ export default async function* storybookExecutor( let frameworkPath = getStorybookFrameworkPath(options.uiFramework); const frameworkOptions = (await import(frameworkPath)).default; - options = normalizeAngularBuilderStylesOptions(options, options.uiFramework); const option = storybookOptionMapper(options, frameworkOptions, context); // print warnings diff --git a/packages/storybook/src/executors/utils.ts b/packages/storybook/src/executors/utils.ts index f9f6e0d0fc069..744a8ded9010c 100644 --- a/packages/storybook/src/executors/utils.ts +++ b/packages/storybook/src/executors/utils.ts @@ -1,25 +1,10 @@ -import { - ExecutorContext, - joinPathFragments, - logger, - parseTargetString, - readProjectConfiguration, - readTargetOptions, - TargetConfiguration, - Tree, -} from '@nrwl/devkit'; -import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils'; +import { ExecutorContext, joinPathFragments, logger } from '@nrwl/devkit'; import 'dotenv/config'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; import { gte } from 'semver'; -import { - findOrCreateConfig, - readCurrentWorkspaceStorybookVersionFromExecutor, -} from '../utils/utilities'; -import { StorybookBuilderOptions } from './build-storybook/build-storybook.impl'; +import { findOrCreateConfig } from '../utils/utilities'; import { CommonNxStorybookConfig } from './models'; -import { StorybookExecutorOptions } from './storybook/storybook.impl'; export interface NodePackage { name: string; @@ -28,7 +13,6 @@ export interface NodePackage { export function getStorybookFrameworkPath(uiFramework) { const serverOptionsPaths = { - '@storybook/angular': '@storybook/angular/dist/ts3.9/server/options', '@storybook/react': '@storybook/react/dist/cjs/server/options', '@storybook/html': '@storybook/html/dist/cjs/server/options', '@storybook/vue': '@storybook/vue/dist/cjs/server/options', @@ -54,35 +38,6 @@ function isStorybookV62onwards(uiFramework) { return gte(storybookPackageVersion, '6.2.0-rc.4'); } -// see: https://github.com/storybookjs/storybook/pull/12565 -// TODO: this should really be passed as a param to the CLI rather than env -export function setStorybookAppProject( - context: ExecutorContext, - leadStorybookProject: string -) { - let leadingProject: string; - // for libs we check whether the build config should be fetched - // from some app - - if ( - context.workspace.projects[context.projectName].projectType === 'library' - ) { - // we have a lib so let's try to see whether the app has - // been set from which we want to get the build config - if (leadStorybookProject) { - leadingProject = leadStorybookProject; - } else { - // do nothing - return; - } - } else { - // ..for apps we just use the app target itself - leadingProject = context.projectName; - } - - process.env.STORYBOOK_ANGULAR_PROJECT = leadingProject; -} - export function runStorybookSetupCheck(options: CommonNxStorybookConfig) { webpackFinalPropertyCheck(options); reactWebpack5Check(options); @@ -180,213 +135,5 @@ export function resolveCommonStorybookOptionMapper( watch: false, }; - if ( - builderOptions.uiFramework === '@storybook/angular' && - // just for new 6.4 with Angular - isStorybookGTE6_4() - ) { - let buildProjectName; - let targetName = 'build'; // default - let targetOptions = null; - - if (builderOptions.projectBuildConfig) { - const targetString = normalizeTargetString( - builderOptions.projectBuildConfig, - targetName - ); - - const { project, target, configuration } = - parseTargetString(targetString); - - // set the extracted target name - targetName = target; - buildProjectName = project; - - targetOptions = readTargetOptions( - { project, target, configuration }, - context - ); - - storybookOptions.angularBrowserTarget = targetString; - } else { - const { storybookBuildTarget, storybookTarget, buildTarget } = - findStorybookAndBuildTargets( - context?.workspace?.projects?.[context.projectName]?.targets - ); - - throw new Error( - ` - No projectBuildConfig was provided. - - To fix this, you can try one of the following options: - - 1. You can run the ${ - context.targetName ? context.targetName : storybookTarget - } executor by providing the projectBuildConfig flag as follows: - - nx ${context.targetName ? context.targetName : storybookTarget} ${ - context.projectName - } --projectBuildConfig=${context.projectName}${ - !buildTarget && storybookBuildTarget ? `:${storybookBuildTarget}` : '' - } - - 2. In your project configuration, under the "${ - context.targetName ? context.targetName : storybookTarget - }" target options, you can - set the "projectBuildConfig" property to the name of the project of which you want to use - the build configuration for Storybook. - ` - ); - } - - const project = context.workspace.projects[buildProjectName]; - - const angularDevkitCompatibleLogger = { - ...logger, - createChild() { - return angularDevkitCompatibleLogger; - }, - }; - - // construct a builder object for Storybook - storybookOptions.angularBuilderContext = { - target: { - ...project.targets[targetName], - project: buildProjectName, - }, - workspaceRoot: context.cwd, - getProjectMetadata: () => { - return project; - }, - getTargetOptions: () => { - return targetOptions; - }, - logger: angularDevkitCompatibleLogger, - }; - - // Add watch to angularBuilderOptions for Storybook to merge configs correctly - storybookOptions.angularBuilderOptions = { - watch: true, - }; - } else { - // keep the backwards compatibility - setStorybookAppProject(context, builderOptions.projectBuildConfig); - } - return storybookOptions; } - -function normalizeTargetString( - appName: string, - defaultTarget: string = 'build' -) { - if (appName?.includes(':')) { - return appName; - } - return `${appName}:${defaultTarget}`; -} - -function isStorybookGTE6_4() { - const storybookVersion = readCurrentWorkspaceStorybookVersionFromExecutor(); - - return gte( - checkAndCleanWithSemver('@storybook/core', storybookVersion), - '6.4.0-rc.1' - ); -} - -export function customProjectBuildConfigIsValid( - tree: Tree, - projectBuildConfig: string -): boolean { - if (projectBuildConfig?.includes(':')) { - const { project, target } = parseTargetString(projectBuildConfig); - const projectConfig = readProjectConfiguration(tree, project); - if (projectConfig?.targets?.[target]) { - return true; - } else { - logger.warn(` - The projectBuildConfig you provided is not valid. - ${!projectConfig ? 'The project ' + project + ' does not exist.' : ''} - ${ - projectConfig && - !projectConfig.targets?.[target] && - `The project ${project} does not have the target ${target}.` - } - The default projectBuildConfig is going to be used. - `); - } - } else { - try { - return Boolean(readProjectConfiguration(tree, projectBuildConfig)); - } catch (e) { - logger.warn(` - The projectBuildConfig you provided is not valid. - The project ${projectBuildConfig} does not exist. - The default projectBuildConfig is going to be used. - `); - } - } -} - -export function findStorybookAndBuildTargets(targets: { - [targetName: string]: TargetConfiguration; -}): { - storybookBuildTarget?: string; - storybookTarget?: string; - buildTarget?: string; -} { - const returnObject: { - storybookBuildTarget?: string; - storybookTarget?: string; - buildTarget?: string; - } = {}; - Object.entries(targets).forEach(([target, targetConfig]) => { - if (targetConfig.executor === '@nrwl/storybook:storybook') { - returnObject.storybookTarget = target; - } - if (targetConfig.executor === '@nrwl/storybook:build') { - returnObject.storybookBuildTarget = target; - } - /** - * Not looking for '@nrwl/angular:ng-packagr-lite', only - * looking for '@angular-devkit/build-angular:browser' - * because the '@nrwl/angular:ng-packagr-lite' executor - * does not support styles and extra options, so the user - * will be forced to switch to build-storybook to add extra options. - * - * So we might as well use the build-storybook by default to - * avoid any errors. - */ - if (targetConfig.executor === '@angular-devkit/build-angular:browser') { - returnObject.buildTarget = target; - } - }); - return returnObject; -} - -export function normalizeAngularBuilderStylesOptions( - builderOptions: StorybookBuilderOptions | StorybookExecutorOptions, - uiFramework: - | '@storybook/angular' - | '@storybook/react' - | '@storybook/html' - | '@storybook/web-components' - | '@storybook/vue' - | '@storybook/vue3' - | '@storybook/svelte' - | '@storybook/react-native' -): StorybookBuilderOptions | StorybookExecutorOptions { - if ( - uiFramework !== '@storybook/angular' && - uiFramework !== '@storybook/react' - ) { - if (builderOptions.styles) { - delete builderOptions.styles; - } - if (builderOptions.stylePreprocessorOptions) { - delete builderOptions.stylePreprocessorOptions; - } - } - return builderOptions; -} diff --git a/packages/storybook/src/generators/change-storybook-targets/__snapshots__/change-storybook-targets.spec.ts.snap b/packages/storybook/src/generators/change-storybook-targets/__snapshots__/change-storybook-targets.spec.ts.snap new file mode 100644 index 0000000000000..add08ae099a34 --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/__snapshots__/change-storybook-targets.spec.ts.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Change the Storybook targets for Angular projects to use native Storybooke executor for all types of angular projects - non-buildable and buildable libs/apps should keep any extra options added in the target 1`] = ` +Object { + "affected": Object { + "defaultBase": "main", + }, + "npmScope": "proj", + "tasksRunnerOptions": Object { + "default": Object { + "options": Object { + "cacheableOperations": Array [ + "build", + "lint", + "test", + "e2e", + ], + }, + "runner": "nx/tasks-runners/default", + }, + }, + "version": 1, +} +`; + +exports[`Change the Storybook targets for Angular projects to use native Storybooke executor for all types of angular projects - non-buildable and buildable libs/apps should set the browserTarget correctly even if target names are not the default 1`] = ` +Object { + "affected": Object { + "defaultBase": "main", + }, + "npmScope": "proj", + "tasksRunnerOptions": Object { + "default": Object { + "options": Object { + "cacheableOperations": Array [ + "build", + "lint", + "test", + "e2e", + ], + }, + "runner": "nx/tasks-runners/default", + }, + }, + "version": 1, +} +`; + +exports[`Change the Storybook targets for Angular projects to use native Storybooke executor for all types of angular projects - non-buildable and buildable libs/apps should set the browserTarget correctly in the Storybook config according to the type of project 1`] = ` +Object { + "affected": Object { + "defaultBase": "main", + }, + "npmScope": "proj", + "tasksRunnerOptions": Object { + "default": Object { + "options": Object { + "cacheableOperations": Array [ + "build", + "lint", + "test", + "e2e", + ], + }, + "runner": "nx/tasks-runners/default", + }, + }, + "version": 1, +} +`; + +exports[`Change the Storybook targets for Angular projects to use native Storybooke executor for non-angular projects should not change their Storybook targets 1`] = ` +Object { + "affected": Object { + "defaultBase": "main", + }, + "npmScope": "proj", + "tasksRunnerOptions": Object { + "default": Object { + "options": Object { + "cacheableOperations": Array [ + "build", + "lint", + "test", + "e2e", + ], + }, + "runner": "nx/tasks-runners/default", + }, + }, +} +`; diff --git a/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.spec.ts b/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.spec.ts new file mode 100644 index 0000000000000..28c352d4d0d22 --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.spec.ts @@ -0,0 +1,47 @@ +import { readWorkspaceConfiguration, Tree, writeJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import changeStorybookTargetsGenerator from './change-storybook-targets'; +import * as defaultConfig from './test-configs/default-config.json'; +import * as customNames from './test-configs/custom-names-config.json'; +import * as nonAngular from './test-configs/non-angular.json'; +import * as extraOptions from './test-configs/extra-options-for-storybook.json'; + +describe('Change the Storybook targets for Angular projects to use native Storybooke executor', () => { + let tree: Tree; + + describe('for all types of angular projects - non-buildable and buildable libs/apps', () => { + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + }); + + it(`should set the browserTarget correctly in the Storybook config according to the type of project`, async () => { + writeJson(tree, 'workspace.json', defaultConfig); + await changeStorybookTargetsGenerator(tree); + expect(readWorkspaceConfiguration(tree)).toMatchSnapshot(); + }); + + it(`should set the browserTarget correctly even if target names are not the default`, async () => { + writeJson(tree, 'workspace.json', customNames); + await changeStorybookTargetsGenerator(tree); + expect(readWorkspaceConfiguration(tree)).toMatchSnapshot(); + }); + + it(`should keep any extra options added in the target`, async () => { + writeJson(tree, 'workspace.json', extraOptions); + await changeStorybookTargetsGenerator(tree); + expect(readWorkspaceConfiguration(tree)).toMatchSnapshot(); + }); + }); + + describe('for non-angular projects', () => { + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + writeJson(tree, 'workspace.json', nonAngular); + }); + + it(`should not change their Storybook targets`, async () => { + await changeStorybookTargetsGenerator(tree); + expect(readWorkspaceConfiguration(tree)).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.ts b/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.ts new file mode 100644 index 0000000000000..d78d3fd6b220d --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/change-storybook-targets.ts @@ -0,0 +1,166 @@ +import { + logger, + Tree, + formatFiles, + updateProjectConfiguration, + getProjects, + TargetConfiguration, + ProjectConfiguration, + Target, + convertNxGenerator, +} from '@nrwl/devkit'; +import { findStorybookAndBuildTargets } from '../../utils/utilities'; + +export async function changeStorybookTargetsGenerator(tree: Tree) { + let changesMade = false; + let changesMadeToAtLeastOne = false; + const projects = getProjects(tree); + [...projects.entries()].forEach(([projectName, projectConfiguration]) => { + changesMade = false; + const { storybookBuildTarget, storybookTarget, buildTarget } = + findStorybookAndBuildTargets(projectConfiguration.targets); + if ( + projectName && + storybookTarget && + projectConfiguration?.targets?.[storybookTarget]?.options?.uiFramework === + '@storybook/angular' + ) { + projectConfiguration.targets[storybookTarget] = updateStorybookTarget( + projectConfiguration, + storybookTarget, + projectName, + buildTarget, + storybookBuildTarget + ); + changesMade = true; + changesMadeToAtLeastOne = true; + projectConfiguration.targets[storybookBuildTarget] = + updateStorybookBuildTarget( + projectConfiguration, + projectName, + buildTarget, + storybookBuildTarget + ); + } else { + logger.warn(`Could not find a Storybook target for ${projectName}.`); + } + if (changesMade) { + updateProjectConfiguration(tree, projectName, projectConfiguration); + } + }); + + if (changesMadeToAtLeastOne) { + await formatFiles(tree); + } +} + +function updateStorybookTarget( + projectConfiguration: ProjectConfiguration, + storybookTarget: string, + projectName: string, + buildTarget: string, + storybookBuildTarget: string +): TargetConfiguration { + const oldStorybookTargetConfig: TargetConfiguration = + projectConfiguration?.targets?.[storybookTarget]; + const newStorybookTargetConfig: TargetConfiguration = { + executor: '@storybook/angular:start-storybook', + options: { + port: oldStorybookTargetConfig.options.port, + configDir: oldStorybookTargetConfig.options.config?.configFolder, + browserTarget: undefined, + compodoc: false, + }, + configurations: oldStorybookTargetConfig.configurations, + }; + + const { project, target } = parseTargetStringCustom( + oldStorybookTargetConfig.options.projectBuildConfig + ); + if (project && target) { + newStorybookTargetConfig.options.browserTarget = + oldStorybookTargetConfig.options.projectBuildConfig; + } else { + newStorybookTargetConfig.options.browserTarget = `${projectName}:${ + buildTarget ? buildTarget : storybookBuildTarget + }`; + } + + const { + uiFramework, + outputPath, + config, + projectBuildConfig, + ...optionsToCopy + } = oldStorybookTargetConfig.options; + + newStorybookTargetConfig.options = { + ...optionsToCopy, + ...newStorybookTargetConfig.options, + }; + + return newStorybookTargetConfig; +} + +function updateStorybookBuildTarget( + projectConfiguration: ProjectConfiguration, + projectName: string, + buildTarget: string, + storybookBuildTarget: string +): TargetConfiguration { + const oldStorybookBuildTargetConfig: TargetConfiguration = + projectConfiguration?.targets?.[storybookBuildTarget]; + const newStorybookBuildTargetConfig: TargetConfiguration = { + executor: '@storybook/angular:build-storybook', + outputs: oldStorybookBuildTargetConfig.outputs, + options: { + outputDir: oldStorybookBuildTargetConfig.options.outputPath, + configDir: oldStorybookBuildTargetConfig.options.config?.configFolder, + browserTarget: undefined, + compodoc: false, + }, + configurations: oldStorybookBuildTargetConfig.configurations, + }; + + const { project, target } = parseTargetStringCustom( + oldStorybookBuildTargetConfig.options.projectBuildConfig + ); + if (project && target) { + newStorybookBuildTargetConfig.options.browserTarget = + oldStorybookBuildTargetConfig.options.projectBuildConfig; + } else { + newStorybookBuildTargetConfig.options.browserTarget = `${projectName}:${ + buildTarget ? buildTarget : storybookBuildTarget + }`; + } + + const { + uiFramework, + outputPath, + config, + projectBuildConfig, + ...optionsToCopy + } = oldStorybookBuildTargetConfig.options; + + newStorybookBuildTargetConfig.options = { + ...optionsToCopy, + ...newStorybookBuildTargetConfig.options, + }; + + return newStorybookBuildTargetConfig; +} + +function parseTargetStringCustom(targetString: string): Target { + const [project, target, configuration] = targetString.split(':'); + + return { + project, + target, + configuration, + }; +} + +export default changeStorybookTargetsGenerator; +export const changeStorybookTargetsSchematic = convertNxGenerator( + changeStorybookTargetsGenerator +); diff --git a/packages/storybook/src/generators/change-storybook-targets/schema.d.ts b/packages/storybook/src/generators/change-storybook-targets/schema.d.ts new file mode 100644 index 0000000000000..0d9cdac014503 --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/schema.d.ts @@ -0,0 +1 @@ +export interface ChangeStorybookTargets {} diff --git a/packages/storybook/src/generators/change-storybook-targets/schema.json b/packages/storybook/src/generators/change-storybook-targets/schema.json new file mode 100644 index 0000000000000..4cb7d6d0872b8 --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "change-storybook-targets", + "title": "Change Storybook targets", + "description": "Change the Storybook target executors.", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/packages/storybook/src/generators/change-storybook-targets/test-configs/custom-names-config.json b/packages/storybook/src/generators/change-storybook-targets/test-configs/custom-names-config.json new file mode 100644 index 0000000000000..aaca550c33f90 --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/test-configs/custom-names-config.json @@ -0,0 +1,154 @@ +{ + "projects": { + "ui-one": { + "projectType": "library", + "root": "libs/ui/one", + "sourceRoot": "libs/ui/one/src", + "targets": { + "trthrngb": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/one/.storybook" + }, + "projectBuildConfig": "ui-one:asdgsdfg" + } + }, + "asdgsdfg": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/one", + "config": { + "configFolder": "libs/ui/one/.storybook" + }, + "projectBuildConfig": "ui-one:asdgsdfg" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "ui-two": { + "projectType": "library", + "root": "libs/ui/two", + "sourceRoot": "libs/ui/two/src", + "targets": { + "sdft": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/two/.storybook" + }, + "projectBuildConfig": "ui-two:thjkkb" + } + }, + "thjkkb": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/two", + "config": { + "configFolder": "libs/ui/two/.storybook" + }, + "projectBuildConfig": "ui-two:thjkkb" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "ui-three": { + "projectType": "library", + "root": "libs/ui/three", + "sourceRoot": "libs/ui/three/src", + "targets": { + "nmkgd": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/three/.storybook" + }, + "projectBuildConfig": "ui-three:aaaa" + } + }, + "aaaa": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/three", + "config": { + "configFolder": "libs/ui/three/.storybook" + }, + "projectBuildConfig": "ui-three:aaaa" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "main-app": { + "projectType": "application", + "root": "apps/main-app", + "sourceRoot": "apps/main-app/src", + "prefix": "katst", + "architect": { + "njdfvndfjnv": { + "builder": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"] + }, + "lmfkcn": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "apps/main-app/.storybook" + }, + "projectBuildConfig": "main-app" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "odmwjbc": { + "builder": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/main-app", + "config": { + "configFolder": "apps/main-app/.storybook" + }, + "projectBuildConfig": "main-app" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + } + } +} diff --git a/packages/storybook/src/generators/change-storybook-targets/test-configs/default-config.json b/packages/storybook/src/generators/change-storybook-targets/test-configs/default-config.json new file mode 100644 index 0000000000000..c47d7f7453e3e --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/test-configs/default-config.json @@ -0,0 +1,154 @@ +{ + "projects": { + "ui-one": { + "projectType": "library", + "root": "libs/ui/one", + "sourceRoot": "libs/ui/one/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/one/.storybook" + }, + "projectBuildConfig": "ui-one:build-storybook" + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/one", + "config": { + "configFolder": "libs/ui/one/.storybook" + }, + "projectBuildConfig": "ui-one:build-storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "ui-two": { + "projectType": "library", + "root": "libs/ui/two", + "sourceRoot": "libs/ui/two/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/two/.storybook" + }, + "projectBuildConfig": "ui-two:build-storybook" + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/two", + "config": { + "configFolder": "libs/ui/two/.storybook" + }, + "projectBuildConfig": "ui-two:build-storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "ui-three": { + "projectType": "library", + "root": "libs/ui/three", + "sourceRoot": "libs/ui/three/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/three/.storybook" + }, + "projectBuildConfig": "ui-three:build-storybook" + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/three", + "config": { + "configFolder": "libs/ui/three/.storybook" + }, + "projectBuildConfig": "ui-three:build-storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "main-app": { + "projectType": "application", + "root": "apps/main-app", + "sourceRoot": "apps/main-app/src", + "prefix": "katst", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"] + }, + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "apps/main-app/.storybook" + }, + "projectBuildConfig": "main-app" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "builder": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/main-app", + "config": { + "configFolder": "apps/main-app/.storybook" + }, + "projectBuildConfig": "main-app" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + } + } +} diff --git a/packages/storybook/src/generators/change-storybook-targets/test-configs/extra-options-for-storybook.json b/packages/storybook/src/generators/change-storybook-targets/test-configs/extra-options-for-storybook.json new file mode 100644 index 0000000000000..37396f85b30cd --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/test-configs/extra-options-for-storybook.json @@ -0,0 +1,222 @@ +{ + "projects": { + "ui-one": { + "projectType": "library", + "root": "libs/ui/one", + "sourceRoot": "libs/ui/one/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/one/.storybook" + }, + "projectBuildConfig": "ui-one:build-storybook", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/one", + "config": { + "configFolder": "libs/ui/one/.storybook" + }, + "projectBuildConfig": "ui-one:build-storybook", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false, + "loglevel": "info" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "ui-two": { + "projectType": "library", + "root": "libs/ui/two", + "sourceRoot": "libs/ui/two/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/two/.storybook" + }, + "projectBuildConfig": "ui-two:build-storybook", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/two", + "config": { + "configFolder": "libs/ui/two/.storybook" + }, + "projectBuildConfig": "ui-two:build-storybook", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false, + "loglevel": "info" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "ui-three": { + "projectType": "library", + "root": "libs/ui/three", + "sourceRoot": "libs/ui/three/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "libs/ui/three/.storybook" + }, + "projectBuildConfig": "ui-three:build-storybook", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/three", + "config": { + "configFolder": "libs/ui/three/.storybook" + }, + "projectBuildConfig": "ui-three:build-storybook", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false, + "loglevel": "info" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "main-app": { + "projectType": "application", + "root": "apps/main-app", + "sourceRoot": "apps/main-app/src", + "prefix": "katst", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"] + }, + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/angular", + "port": 4400, + "config": { + "configFolder": "apps/main-app/.storybook" + }, + "projectBuildConfig": "main-app", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "builder": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/main-app", + "config": { + "configFolder": "apps/main-app/.storybook" + }, + "projectBuildConfig": "main-app", + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@storybook/addon-storysource/node_modules/highlight.js/styles/github.css" + ] + }, + "styles": ["apps/webng/src/styles.css"], + "compodocArgs": ["-e", "json"], + "docsMode": false, + "loglevel": "info" + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + } + } +} diff --git a/packages/storybook/src/generators/change-storybook-targets/test-configs/non-angular.json b/packages/storybook/src/generators/change-storybook-targets/test-configs/non-angular.json new file mode 100644 index 0000000000000..db81e6f301526 --- /dev/null +++ b/packages/storybook/src/generators/change-storybook-targets/test-configs/non-angular.json @@ -0,0 +1,60 @@ +{ + "projects": { + "ui-one": { + "projectType": "library", + "root": "libs/ui/one", + "sourceRoot": "libs/ui/one/src", + "targets": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/react", + "port": 4400, + "config": { + "configFolder": "libs/ui/one/.storybook" + } + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/angular", + "outputPath": "dist/storybook/ui/one", + "config": { + "configFolder": "libs/ui/one/.storybook" + } + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + }, + "main-app": { + "projectType": "application", + "root": "apps/main-app", + "sourceRoot": "apps/main-app/src", + "prefix": "katst", + "architect": { + "storybook": { + "builder": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/react", + "port": 4400, + "config": { + "configFolder": "apps/main-app/.storybook" + } + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } + } + } +} diff --git a/packages/storybook/src/generators/configuration/configuration.spec.ts b/packages/storybook/src/generators/configuration/configuration.spec.ts index 466c572cc1e7c..3d4d834ca78d7 100644 --- a/packages/storybook/src/generators/configuration/configuration.spec.ts +++ b/packages/storybook/src/generators/configuration/configuration.spec.ts @@ -9,20 +9,8 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Linter } from '@nrwl/linter'; import { libraryGenerator } from '@nrwl/workspace/generators'; -import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter'; import { TsConfig } from '../../utils/utilities'; import configurationGenerator from './configuration'; -import { nxVersion, storybookVersion } from '../../utils/versions'; - -const runAngularLibrarySchematic = wrapAngularDevkitSchematic( - '@schematics/angular', - 'library' -); - -const runAngularApplicationSchematic = wrapAngularDevkitSchematic( - '@schematics/angular', - 'application' -); describe('@nrwl/storybook:configuration', () => { let tree: Tree; @@ -211,7 +199,7 @@ describe('@nrwl/storybook:configuration', () => { const project = readProjectConfiguration(tree, 'test-ui-lib-2'); expect(project.targets.storybook).toEqual({ - executor: '@nrwl/storybook:storybook', + executor: '@storybook/angular:start-storybook', configurations: { ci: { quiet: true, @@ -219,11 +207,9 @@ describe('@nrwl/storybook:configuration', () => { }, options: { port: 4400, - projectBuildConfig: 'test-ui-lib-2:build-storybook', - uiFramework: '@storybook/angular', - config: { - configFolder: 'libs/test-ui-lib-2/.storybook', - }, + browserTarget: 'test-ui-lib-2:build-storybook', + compodoc: false, + configDir: 'libs/test-ui-lib-2/.storybook', }, }); @@ -251,7 +237,7 @@ describe('@nrwl/storybook:configuration', () => { const project = readProjectConfiguration(tree, 'test-ui-lib-5'); expect(project.targets.storybook).toEqual({ - executor: '@nrwl/storybook:storybook', + executor: '@storybook/angular:start-storybook', configurations: { ci: { quiet: true, @@ -259,11 +245,9 @@ describe('@nrwl/storybook:configuration', () => { }, options: { port: 4400, - projectBuildConfig: 'test-ui-lib-5:build-storybook', - uiFramework: '@storybook/angular', - config: { - configFolder: 'libs/test-ui-lib-5/.storybook', - }, + browserTarget: 'test-ui-lib-5:build-storybook', + compodoc: false, + configDir: 'libs/test-ui-lib-5/.storybook', }, }); @@ -276,109 +260,6 @@ describe('@nrwl/storybook:configuration', () => { }); }); - it('should update workspace file for angular libs with custom projectBuildConfig', async () => { - const newTree = createTreeWithEmptyWorkspace(); - - await runAngularLibrarySchematic(newTree, { - name: 'ui-lib', - }); - - await runAngularApplicationSchematic(newTree, { - name: 'test-app', - }); - - writeJson(newTree, 'package.json', { - devDependencies: { - '@nrwl/storybook': nxVersion, - '@storybook/addon-essentials': storybookVersion, - '@storybook/angular': storybookVersion, - }, - }); - - writeJson(newTree, 'ui-lib/tsconfig.json', {}); - writeJson(newTree, 'test-app/tsconfig.json', {}); - - await configurationGenerator(newTree, { - name: 'ui-lib', - uiFramework: '@storybook/angular', - standaloneConfig: false, - projectBuildConfig: 'test-app', - }); - - const project = readProjectConfiguration(newTree, 'ui-lib'); - - expect(project.targets.storybook?.options?.projectBuildConfig).toBe( - 'test-app' - ); - }); - - it('should update workspace file for angular libs with default projectBuildConfig if the one provided is invalid', async () => { - const newTree = createTreeWithEmptyWorkspace(); - - await runAngularLibrarySchematic(newTree, { - name: 'ui-lib', - }); - - await runAngularApplicationSchematic(newTree, { - name: 'test-app', - }); - - writeJson(newTree, 'package.json', { - devDependencies: { - '@nrwl/storybook': nxVersion, - '@storybook/addon-essentials': storybookVersion, - '@storybook/angular': storybookVersion, - }, - }); - - writeJson(newTree, 'ui-lib/tsconfig.json', {}); - writeJson(newTree, 'test-app/tsconfig.json', {}); - - await configurationGenerator(newTree, { - name: 'ui-lib', - uiFramework: '@storybook/angular', - standaloneConfig: false, - projectBuildConfig: 'test-app:asdfasdf', - }); - - const project = readProjectConfiguration(newTree, 'ui-lib'); - - expect(project.targets.storybook?.options?.projectBuildConfig).toBe( - 'ui-lib:build-storybook' - ); - }); - - it('should update workspace file for angular libs with default projectBuildConfig if the project provided does not exist', async () => { - const newTree = createTreeWithEmptyWorkspace(); - - await runAngularLibrarySchematic(newTree, { - name: 'ui-lib', - }); - - writeJson(newTree, 'package.json', { - devDependencies: { - '@nrwl/storybook': nxVersion, - '@storybook/addon-essentials': storybookVersion, - '@storybook/angular': storybookVersion, - }, - }); - - writeJson(newTree, 'ui-lib/tsconfig.json', {}); - - await configurationGenerator(newTree, { - name: 'ui-lib', - uiFramework: '@storybook/angular', - standaloneConfig: false, - projectBuildConfig: 'test-app', - }); - - const project = readProjectConfiguration(newTree, 'ui-lib'); - - expect(project.targets.storybook?.options?.projectBuildConfig).toBe( - 'ui-lib:build-storybook' - ); - }); - it('should update `tsconfig.lib.json` file', async () => { await configurationGenerator(tree, { name: 'test-ui-lib', diff --git a/packages/storybook/src/generators/configuration/configuration.ts b/packages/storybook/src/generators/configuration/configuration.ts index 9a505d2cf9aea..b5f80100b7e64 100644 --- a/packages/storybook/src/generators/configuration/configuration.ts +++ b/packages/storybook/src/generators/configuration/configuration.ts @@ -1,39 +1,28 @@ import { convertNxGenerator, formatFiles, - generateFiles, GeneratorCallback, - joinPathFragments, logger, - offsetFromRoot, - readJson, readProjectConfiguration, - toJS, Tree, - updateJson, - updateProjectConfiguration, - writeJson, } from '@nrwl/devkit'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; -import { Linter } from '@nrwl/linter'; -import { join } from 'path'; - -import { - isFramework, - readCurrentWorkspaceStorybookVersionFromGenerator, - TsConfig, -} from '../../utils/utilities'; import { cypressProjectGenerator } from '../cypress-project/cypress-project'; import { StorybookConfigureSchema } from './schema'; import { initGenerator } from '../init/init'; -import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils'; -import { gte } from 'semver'; + import { - customProjectBuildConfigIsValid, - findStorybookAndBuildTargets, -} from '../../executors/utils'; -import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; + addAngularStorybookTask, + addStorybookTask, + configureTsProjectConfig, + configureTsSolutionConfig, + createProjectStorybookDir, + createRootStorybookDir, + updateLintConfig, +} from './util-functions'; +import { Linter } from '@nrwl/linter'; +import { findStorybookAndBuildTargets } from '../../utils/utilities'; export async function configurationGenerator( tree: Tree, @@ -44,9 +33,7 @@ export async function configurationGenerator( const tasks: GeneratorCallback[] = []; const { projectType, targets } = readProjectConfiguration(tree, schema.name); - const { buildTarget } = findStorybookAndBuildTargets(targets); - const initTask = await initGenerator(tree, { uiFramework: schema.uiFramework, }); @@ -57,13 +44,13 @@ export async function configurationGenerator( configureTsProjectConfig(tree, schema); configureTsSolutionConfig(tree, schema); updateLintConfig(tree, schema); - addStorybookTask( - tree, - schema.name, - schema.uiFramework, - buildTarget, - schema.projectBuildConfig - ); + + if (schema.uiFramework === '@storybook/angular') { + addAngularStorybookTask(tree, schema.name, buildTarget); + } else { + addStorybookTask(tree, schema.name, schema.uiFramework); + } + if (schema.configureCypress) { if (projectType !== 'application') { const cypressTask = await cypressProjectGenerator(tree, { @@ -98,282 +85,6 @@ function normalizeSchema( }; } -function createRootStorybookDir(tree: Tree, js: boolean) { - if (tree.exists('.storybook')) { - logger.warn( - `.storybook folder already exists at root! Skipping generating files in it.` - ); - return; - } - logger.debug(`adding .storybook folder to the root directory`); - - const templatePath = join(__dirname, './root-files'); - generateFiles(tree, templatePath, '', { - rootTsConfigPath: getRootTsConfigPathInTree(tree), - }); - - if (js) { - toJS(tree); - } -} - -function createProjectStorybookDir( - tree: Tree, - projectName: string, - uiFramework: StorybookConfigureSchema['uiFramework'], - js: boolean -) { - const { root, projectType } = readProjectConfiguration(tree, projectName); - const projectDirectory = projectType === 'application' ? 'app' : 'lib'; - - const storybookRoot = join(root, '.storybook'); - - if (tree.exists(storybookRoot)) { - logger.warn( - `.storybook folder already exists for ${projectName}! Skipping generating files in it.` - ); - return; - } - - logger.debug(`adding .storybook folder to ${projectDirectory}`); - const templatePath = join(__dirname, './project-files'); - - generateFiles(tree, templatePath, root, { - tmpl: '', - uiFramework, - offsetFromRoot: offsetFromRoot(root), - rootTsConfigPath: getRootTsConfigPathInTree(tree), - projectType: projectDirectory, - useWebpack5: - uiFramework === '@storybook/angular' || - uiFramework === '@storybook/react', - existsRootWebpackConfig: tree.exists('.storybook/webpack.config.js'), - }); - - if (js) { - toJS(tree); - } -} - -function getTsConfigPath( - tree: Tree, - projectName: string, - path?: string -): string { - const { root, projectType } = readProjectConfiguration(tree, projectName); - return join( - root, - path && path.length > 0 - ? path - : projectType === 'application' - ? 'tsconfig.app.json' - : 'tsconfig.lib.json' - ); -} - -function configureTsProjectConfig( - tree: Tree, - schema: StorybookConfigureSchema -) { - const { name: projectName } = schema; - - let tsConfigPath: string; - let tsConfigContent: TsConfig; - - try { - tsConfigPath = getTsConfigPath(tree, projectName); - tsConfigContent = readJson(tree, tsConfigPath); - } catch { - /** - * Custom app configurations - * may contain a tsconfig.json - * instead of a tsconfig.app.json. - */ - - tsConfigPath = getTsConfigPath(tree, projectName, 'tsconfig.json'); - tsConfigContent = readJson(tree, tsConfigPath); - } - - if ( - !tsConfigContent?.exclude?.includes('**/*.stories.ts') && - !tsConfigContent?.exclude?.includes('**/*.stories.js') - ) { - tsConfigContent.exclude = [ - ...(tsConfigContent.exclude || []), - '**/*.stories.ts', - '**/*.stories.js', - ...(isFramework('react', schema) || isFramework('react-native', schema) - ? ['**/*.stories.jsx', '**/*.stories.tsx'] - : []), - ]; - } - - writeJson(tree, tsConfigPath, tsConfigContent); -} - -function configureTsSolutionConfig( - tree: Tree, - schema: StorybookConfigureSchema -) { - const { name: projectName } = schema; - - const { root } = readProjectConfiguration(tree, projectName); - const tsConfigPath = join(root, 'tsconfig.json'); - const tsConfigContent = readJson(tree, tsConfigPath); - - if ( - !tsConfigContent?.references - ?.map((reference) => reference.path) - ?.includes('./.storybook/tsconfig.json') - ) { - tsConfigContent.references = [ - ...(tsConfigContent.references || []), - { - path: './.storybook/tsconfig.json', - }, - ]; - } - - writeJson(tree, tsConfigPath, tsConfigContent); -} - -/** - * When adding storybook we need to inform TSLint or ESLint - * of the additional tsconfig.json file which will be the only tsconfig - * which includes *.stories files. - * - * For TSLint this is done via the builder config, for ESLint this is - * done within the .eslintrc.json file. - */ -function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { - const { name: projectName } = schema; - - const { targets, root } = readProjectConfiguration(tree, projectName); - const tslintTargets = Object.values(targets).filter( - (target) => target.executor === '@angular-devkit/build-angular:tslint' - ); - - tslintTargets.forEach((target) => { - target.options.tsConfig = dedupe([ - ...target.options.tsConfig, - joinPathFragments(root, './.storybook/tsconfig.json'), - ]); - }); - - if (tree.exists(join(root, '.eslintrc.json'))) { - updateJson(tree, join(root, '.eslintrc.json'), (json) => { - if (typeof json.parserOptions?.project === 'string') { - json.parserOptions.project = [json.parserOptions.project]; - } - - if (Array.isArray(json.parserOptions?.project)) { - json.parserOptions.project = dedupe([ - ...json.parserOptions.project, - join(root, '.storybook/tsconfig.json'), - ]); - } - - const overrides = json.overrides || []; - for (const o of overrides) { - if (typeof o.parserOptions?.project === 'string') { - o.parserOptions.project = [o.parserOptions.project]; - } - if (Array.isArray(o.parserOptions?.project)) { - o.parserOptions.project = dedupe([ - ...o.parserOptions.project, - join(root, '.storybook/tsconfig.json'), - ]); - } - } - - return json; - }); - } -} - -function dedupe(arr: string[]) { - return Array.from(new Set(arr)); -} - -function addStorybookTask( - tree: Tree, - projectName: string, - uiFramework: string, - buildTargetForAngularProjects: string, - customProjectBuildConfig?: string -) { - if (uiFramework === '@storybook/react-native') { - return; - } - const projectConfig = readProjectConfiguration(tree, projectName); - projectConfig.targets['storybook'] = { - executor: '@nrwl/storybook:storybook', - options: { - uiFramework, - port: 4400, - config: { - configFolder: `${projectConfig.root}/.storybook`, - }, - projectBuildConfig: - uiFramework === '@storybook/angular' - ? customProjectBuildConfig && - customProjectBuildConfigIsValid(tree, customProjectBuildConfig) - ? customProjectBuildConfig - : buildTargetForAngularProjects - ? projectName - : `${projectName}:build-storybook` - : undefined, - }, - configurations: { - ci: { - quiet: true, - }, - }, - }; - projectConfig.targets['build-storybook'] = { - executor: '@nrwl/storybook:build', - outputs: ['{options.outputPath}'], - options: { - uiFramework, - outputPath: joinPathFragments('dist/storybook', projectName), - config: { - configFolder: `${projectConfig.root}/.storybook`, - }, - projectBuildConfig: - uiFramework === '@storybook/angular' - ? customProjectBuildConfig && - customProjectBuildConfigIsValid(tree, customProjectBuildConfig) - ? customProjectBuildConfig - : buildTargetForAngularProjects - ? projectName - : `${projectName}:build-storybook` - : undefined, - }, - configurations: { - ci: { - quiet: true, - }, - }, - }; - - updateProjectConfiguration(tree, projectName, projectConfig); -} - -function getCurrentWorkspaceStorybookVersion(tree: Tree): string { - let workspaceStorybookVersion = - readCurrentWorkspaceStorybookVersionFromGenerator(tree); - - if ( - gte( - checkAndCleanWithSemver('@storybook/core', workspaceStorybookVersion), - '6.0.0' - ) - ) { - workspaceStorybookVersion = '6'; - } - return workspaceStorybookVersion; -} - export default configurationGenerator; export const configurationSchematic = convertNxGenerator( configurationGenerator diff --git a/packages/storybook/src/generators/configuration/util-functions.ts b/packages/storybook/src/generators/configuration/util-functions.ts new file mode 100644 index 0000000000000..c7f7f842d7a06 --- /dev/null +++ b/packages/storybook/src/generators/configuration/util-functions.ts @@ -0,0 +1,312 @@ +import { + generateFiles, + joinPathFragments, + logger, + offsetFromRoot, + readJson, + readProjectConfiguration, + toJS, + Tree, + updateJson, + updateProjectConfiguration, + writeJson, +} from '@nrwl/devkit'; +import { Linter } from '@nrwl/linter'; +import { join } from 'path'; +import { dedupe, isFramework, TsConfig } from '../../utils/utilities'; +import { StorybookConfigureSchema } from './schema'; +import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; + +export function addStorybookTask( + tree: Tree, + projectName: string, + uiFramework: string +) { + if (uiFramework === '@storybook/react-native') { + return; + } + const projectConfig = readProjectConfiguration(tree, projectName); + projectConfig.targets['storybook'] = { + executor: '@nrwl/storybook:storybook', + options: { + uiFramework, + port: 4400, + config: { + configFolder: `${projectConfig.root}/.storybook`, + }, + }, + configurations: { + ci: { + quiet: true, + }, + }, + }; + projectConfig.targets['build-storybook'] = { + executor: '@nrwl/storybook:build', + outputs: ['{options.outputPath}'], + options: { + uiFramework, + outputPath: joinPathFragments('dist/storybook', projectName), + config: { + configFolder: `${projectConfig.root}/.storybook`, + }, + }, + configurations: { + ci: { + quiet: true, + }, + }, + }; + + updateProjectConfiguration(tree, projectName, projectConfig); +} + +export function addAngularStorybookTask( + tree: Tree, + projectName: string, + buildTargetForAngularProjects: string +) { + const projectConfig = readProjectConfiguration(tree, projectName); + projectConfig.targets['storybook'] = { + executor: '@storybook/angular:start-storybook', + options: { + port: 4400, + configDir: `${projectConfig.root}/.storybook`, + browserTarget: `${projectName}:${ + buildTargetForAngularProjects ? 'build' : 'build-storybook' + }`, + compodoc: false, + }, + configurations: { + ci: { + quiet: true, + }, + }, + }; + projectConfig.targets['build-storybook'] = { + executor: '@storybook/angular:build-storybook', + outputs: ['{options.outputPath}'], + options: { + outputDir: joinPathFragments('dist/storybook', projectName), + configDir: `${projectConfig.root}/.storybook`, + browserTarget: `${projectName}:${ + buildTargetForAngularProjects ? 'build' : 'build-storybook' + }`, + compodoc: false, + }, + configurations: { + ci: { + quiet: true, + }, + }, + }; + + updateProjectConfiguration(tree, projectName, projectConfig); +} + +export function configureTsProjectConfig( + tree: Tree, + schema: StorybookConfigureSchema +) { + const { name: projectName } = schema; + + let tsConfigPath: string; + let tsConfigContent: TsConfig; + + try { + tsConfigPath = getTsConfigPath(tree, projectName); + tsConfigContent = readJson(tree, tsConfigPath); + } catch { + /** + * Custom app configurations + * may contain a tsconfig.json + * instead of a tsconfig.app.json. + */ + + tsConfigPath = getTsConfigPath(tree, projectName, 'tsconfig.json'); + tsConfigContent = readJson(tree, tsConfigPath); + } + + if ( + !tsConfigContent.exclude.includes('**/*.stories.ts') && + !tsConfigContent.exclude.includes('**/*.stories.js') + ) { + tsConfigContent.exclude = [ + ...(tsConfigContent.exclude || []), + '**/*.stories.ts', + '**/*.stories.js', + ...(isFramework('react', schema) || isFramework('react-native', schema) + ? ['**/*.stories.jsx', '**/*.stories.tsx'] + : []), + ]; + } + + writeJson(tree, tsConfigPath, tsConfigContent); +} + +export function configureTsSolutionConfig( + tree: Tree, + schema: StorybookConfigureSchema +) { + const { name: projectName } = schema; + + const { root } = readProjectConfiguration(tree, projectName); + const tsConfigPath = join(root, 'tsconfig.json'); + const tsConfigContent = readJson(tree, tsConfigPath); + + if ( + !tsConfigContent.references + ?.map((reference) => reference.path) + ?.includes('./.storybook/tsconfig.json') + ) { + tsConfigContent.references = [ + ...(tsConfigContent.references || []), + { + path: './.storybook/tsconfig.json', + }, + ]; + } + + writeJson(tree, tsConfigPath, tsConfigContent); +} + +/** + * When adding storybook we need to inform TSLint or ESLint + * of the additional tsconfig.json file which will be the only tsconfig + * which includes *.stories files. + * + * For TSLint this is done via the builder config, for ESLint this is + * done within the .eslintrc.json file. + */ +export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { + const { name: projectName } = schema; + + const { targets, root } = readProjectConfiguration(tree, projectName); + const tslintTargets = Object.values(targets).filter( + (target) => target.executor === '@angular-devkit/build-angular:tslint' + ); + + tslintTargets.forEach((target) => { + target.options.tsConfig = dedupe([ + ...target.options.tsConfig, + joinPathFragments(root, './.storybook/tsconfig.json'), + ]); + }); + + if (tree.exists(join(root, '.eslintrc.json'))) { + updateJson(tree, join(root, '.eslintrc.json'), (json) => { + if (typeof json.parserOptions?.project === 'string') { + json.parserOptions.project = [json.parserOptions.project]; + } + + if (Array.isArray(json.parserOptions?.project)) { + json.parserOptions.project = dedupe([ + ...json.parserOptions.project, + join(root, '.storybook/tsconfig.json'), + ]); + } + + const overrides = json.overrides || []; + for (const o of overrides) { + if (typeof o.parserOptions?.project === 'string') { + o.parserOptions.project = [o.parserOptions.project]; + } + if (Array.isArray(o.parserOptions?.project)) { + o.parserOptions.project = dedupe([ + ...o.parserOptions.project, + join(root, '.storybook/tsconfig.json'), + ]); + } + } + + return json; + }); + } +} + +export function normalizeSchema( + schema: StorybookConfigureSchema +): StorybookConfigureSchema { + const defaults = { + configureCypress: true, + linter: Linter.TsLint, + js: false, + }; + return { + ...defaults, + ...schema, + }; +} + +export function createRootStorybookDir(tree: Tree, js: boolean) { + if (tree.exists('.storybook')) { + logger.warn( + `.storybook folder already exists at root! Skipping generating files in it.` + ); + return; + } + logger.debug(`adding .storybook folder to the root directory`); + + const templatePath = join(__dirname, './root-files'); + generateFiles(tree, templatePath, '', { + rootTsConfigPath: getRootTsConfigPathInTree(tree), + }); + + if (js) { + toJS(tree); + } +} + +export function createProjectStorybookDir( + tree: Tree, + projectName: string, + uiFramework: StorybookConfigureSchema['uiFramework'], + js: boolean +) { + const { root, projectType } = readProjectConfiguration(tree, projectName); + const projectDirectory = projectType === 'application' ? 'app' : 'lib'; + + const storybookRoot = join(root, '.storybook'); + + if (tree.exists(storybookRoot)) { + logger.warn( + `.storybook folder already exists for ${projectName}! Skipping generating files in it.` + ); + return; + } + + logger.debug(`adding .storybook folder to ${projectDirectory}`); + const templatePath = join(__dirname, './project-files'); + + generateFiles(tree, templatePath, root, { + tmpl: '', + uiFramework, + offsetFromRoot: offsetFromRoot(root), + rootTsConfigPath: getRootTsConfigPathInTree(tree), + projectType: projectDirectory, + useWebpack5: + uiFramework === '@storybook/angular' || + uiFramework === '@storybook/react', + existsRootWebpackConfig: tree.exists('.storybook/webpack.config.js'), + }); + + if (js) { + toJS(tree); + } +} + +export function getTsConfigPath( + tree: Tree, + projectName: string, + path?: string +): string { + const { root, projectType } = readProjectConfiguration(tree, projectName); + return join( + root, + path && path.length > 0 + ? path + : projectType === 'application' + ? 'tsconfig.app.json' + : 'tsconfig.lib.json' + ); +} diff --git a/packages/storybook/src/migrations/update-13-4-6/set-project-build-config.ts b/packages/storybook/src/migrations/update-13-4-6/set-project-build-config.ts index eb437792be0f1..2ad3e7d842b4f 100644 --- a/packages/storybook/src/migrations/update-13-4-6/set-project-build-config.ts +++ b/packages/storybook/src/migrations/update-13-4-6/set-project-build-config.ts @@ -5,7 +5,7 @@ import { updateProjectConfiguration, getProjects, } from '@nrwl/devkit'; -import { findStorybookAndBuildTargets } from '../../executors/utils'; +import { findStorybookAndBuildTargets } from '../../utils/utilities'; export default async function setProjectBuildConfig(tree: Tree) { let changesMade = false; diff --git a/packages/storybook/src/migrations/update-14-0-0/migrate-stories-to-6-2/migrate-stories-to-6-2.spec.ts b/packages/storybook/src/migrations/update-14-0-0/migrate-stories-to-6-2/migrate-stories-to-6-2.spec.ts index e5c45e2e2c29e..1c7e31bae2073 100644 --- a/packages/storybook/src/migrations/update-14-0-0/migrate-stories-to-6-2/migrate-stories-to-6-2.spec.ts +++ b/packages/storybook/src/migrations/update-14-0-0/migrate-stories-to-6-2/migrate-stories-to-6-2.spec.ts @@ -1,4 +1,8 @@ -import type { Tree } from '@nrwl/devkit'; +import { + readProjectConfiguration, + Tree, + updateProjectConfiguration, +} from '@nrwl/devkit'; import { joinPathFragments, writeJson } from '@nrwl/devkit'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { storybookVersion } from '@nrwl/storybook'; @@ -98,6 +102,23 @@ describe('migrate-stories-to-6-2 schematic', () => { }); ` ); + + /** + * This needs to be updated for the following reason: + * - runAngularStorybookSchematic now generates the Storybook targets in + * the project configuration using the @storybook/angular executors + * - this means that the uiFramework property is not used any more + * - that property was used in versions before that so the migration script looks for it + * - the migrate-stories-to-6-2 migrator should have already taken effect in previous versions + * so there is no need to update the generator to look for the new executor as well + */ + const projectConfig = readProjectConfiguration(appTree, 'test-ui-lib'); + projectConfig.targets.storybook.options.uiFramework = + '@storybook/angular'; + projectConfig.targets.storybook.options.config = { + configFolder: projectConfig.targets.storybook.options.configDir, + }; + updateProjectConfiguration(appTree, 'test-ui-lib', projectConfig); }); it('should move the component from the story to parameters.component', async () => { diff --git a/packages/storybook/src/migrations/update-14-1-8/change-storybook-targets.ts b/packages/storybook/src/migrations/update-14-1-8/change-storybook-targets.ts new file mode 100644 index 0000000000000..8ae413e3d013b --- /dev/null +++ b/packages/storybook/src/migrations/update-14-1-8/change-storybook-targets.ts @@ -0,0 +1,6 @@ +import { Tree } from '@nrwl/devkit'; +import { changeStorybookTargetsGenerator } from '../../generators/change-storybook-targets/change-storybook-targets'; + +export default async function changeStorybookTargets(tree: Tree) { + return changeStorybookTargetsGenerator(tree); +} diff --git a/packages/storybook/src/utils/utilities.ts b/packages/storybook/src/utils/utilities.ts index 60d758ad01843..0bb0136124d53 100644 --- a/packages/storybook/src/utils/utilities.ts +++ b/packages/storybook/src/utils/utilities.ts @@ -1,8 +1,8 @@ import { ExecutorContext, - logger, readJson, readJsonFile, + TargetConfiguration, Tree, } from '@nrwl/devkit'; import { CompilerOptions } from 'typescript'; @@ -158,7 +158,7 @@ export function findOrCreateConfig( config: StorybookConfig, context: ExecutorContext ): string { - if (config.configFolder && statSync(config.configFolder).isDirectory()) { + if (config?.configFolder && statSync(config.configFolder).isDirectory()) { return config.configFolder; } else if ( statSync(config.configPath).isFile() && @@ -204,3 +204,94 @@ function createStorybookConfig( ); return tmpFolder; } + +export function dedupe(arr: string[]) { + return Array.from(new Set(arr)); +} + +/** + * This function is only used for ANGULAR projects. + * And it is used for the "old" Storybook/Angular setup, + * where the Nx executor is used. + * + * At the moment, it's used by the migrator to set projectBuildConfig + * and it is also used by the change-storybook-targets generator/migrator + */ +export function findStorybookAndBuildTargets(targets: { + [targetName: string]: TargetConfiguration; +}): { + storybookBuildTarget?: string; + storybookTarget?: string; + buildTarget?: string; +} { + const returnObject: { + storybookBuildTarget?: string; + storybookTarget?: string; + buildTarget?: string; + } = {}; + Object.entries(targets).forEach(([target, targetConfig]) => { + if (targetConfig.executor === '@nrwl/storybook:storybook') { + returnObject.storybookTarget = target; + } + if (targetConfig.executor === '@nrwl/storybook:build') { + returnObject.storybookBuildTarget = target; + } + /** + * Not looking for '@nrwl/angular:ng-packagr-lite', only + * looking for '@angular-devkit/build-angular:browser' + * because the '@nrwl/angular:ng-packagr-lite' executor + * does not support styles and extra options, so the user + * will be forced to switch to build-storybook to add extra options. + * + * So we might as well use the build-storybook by default to + * avoid any errors. + */ + if (targetConfig.executor === '@angular-devkit/build-angular:browser') { + returnObject.buildTarget = target; + } + }); + return returnObject; +} + +/** + * This function is not used at the moment. + * + * However, it may be valuable to create this here, in order to avoid + * any confusion in the future. + * + */ +export function findStorybookAndBuildTargetsForStorybookAngularExecutors(targets: { + [targetName: string]: TargetConfiguration; +}): { + storybookBuildTarget?: string; + storybookTarget?: string; + buildTarget?: string; +} { + const returnObject: { + storybookBuildTarget?: string; + storybookTarget?: string; + buildTarget?: string; + } = {}; + Object.entries(targets).forEach(([target, targetConfig]) => { + if (targetConfig.executor === '@storybook/angular:start-storybook') { + returnObject.storybookTarget = target; + } + if (targetConfig.executor === '@storybook/angular:build-storybook') { + returnObject.storybookBuildTarget = target; + } + /** + * Not looking for '@nrwl/angular:ng-packagr-lite', only + * looking for '@angular-devkit/build-angular:browser' + * because the '@nrwl/angular:ng-packagr-lite' executor + * does not support styles and extra options, so the user + * will be forced to switch to build-storybook to add extra options. + * + * So we might as well use the build-storybook by default to + * avoid any errors. + */ + if (targetConfig.executor === '@angular-devkit/build-angular:browser') { + returnObject.buildTarget = target; + } + }); + return returnObject; +}