feat(vue): storybook configuration generator for vue (#19141)

This commit is contained in:
Katerina Skroumpelou 2023-09-25 13:23:01 -04:00 committed by GitHub
parent 76bc58d407
commit 295ea3fb93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2042 additions and 121 deletions

View File

@ -1711,6 +1711,14 @@
"children": [],
"disableCollapsible": false
},
{
"name": "Set up Storybook for Vue Projects",
"path": "/recipes/storybook/overview-vue",
"id": "overview-vue",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{
"name": "Set up Storybook for Angular Projects",
"path": "/recipes/storybook/overview-angular",
@ -2900,6 +2908,14 @@
"children": [],
"disableCollapsible": false
},
{
"name": "Set up Storybook for Vue Projects",
"path": "/recipes/storybook/overview-vue",
"id": "overview-vue",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{
"name": "Set up Storybook for Angular Projects",
"path": "/recipes/storybook/overview-angular",
@ -2991,6 +3007,14 @@
"children": [],
"disableCollapsible": false
},
{
"name": "Set up Storybook for Vue Projects",
"path": "/recipes/storybook/overview-vue",
"id": "overview-vue",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{
"name": "Set up Storybook for Angular Projects",
"path": "/recipes/storybook/overview-angular",

View File

@ -2133,6 +2133,16 @@
"path": "/recipes/storybook/overview-react",
"tags": ["storybook"]
},
{
"id": "overview-vue",
"name": "Set up Storybook for Vue Projects",
"description": "This guide explains how to set up Storybook for Vue projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-vue",
"itemList": [],
"isExternal": false,
"path": "/recipes/storybook/overview-vue",
"tags": ["storybook"]
},
{
"id": "overview-angular",
"name": "Set up Storybook for Angular Projects",
@ -3616,6 +3626,16 @@
"path": "/recipes/storybook/overview-react",
"tags": ["storybook"]
},
{
"id": "overview-vue",
"name": "Set up Storybook for Vue Projects",
"description": "This guide explains how to set up Storybook for Vue projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-vue",
"itemList": [],
"isExternal": false,
"path": "/recipes/storybook/overview-vue",
"tags": ["storybook"]
},
{
"id": "overview-angular",
"name": "Set up Storybook for Angular Projects",
@ -3731,6 +3751,16 @@
"path": "/recipes/storybook/overview-react",
"tags": ["storybook"]
},
"/recipes/storybook/overview-vue": {
"id": "overview-vue",
"name": "Set up Storybook for Vue Projects",
"description": "This guide explains how to set up Storybook for Vue projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-vue",
"itemList": [],
"isExternal": false,
"path": "/recipes/storybook/overview-vue",
"tags": ["storybook"]
},
"/recipes/storybook/overview-angular": {
"id": "overview-angular",
"name": "Set up Storybook for Angular Projects",

View File

@ -892,6 +892,13 @@
"name": "Set up Storybook for React Projects",
"path": "/recipes/storybook/overview-react"
},
{
"description": "This guide explains how to set up Storybook for Vue projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-vue",
"id": "overview-vue",
"name": "Set up Storybook for Vue Projects",
"path": "/recipes/storybook/overview-vue"
},
{
"description": "This guide explains how to set up Storybook for Angular projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-angular",

View File

@ -53,7 +53,42 @@ You can generate Storybook configuration for an individual project with this com
nx g @nx/storybook:configuration project-name
```
If you are NOT using a framework-specific generator (for [Angular](/nx-api/angular/generators/storybook-configuration), [React](/nx-api/react/generators/storybook-configuration), [React Native](/nx-api/react-native/generators/storybook-configuration)), in the field `uiFramework` you must choose one of the following Storybook frameworks:
or
{% tabs %}
{% tab label="Angular" %}
```shell
nx g @nx/angular:storybook-configuration my-angular-project
```
{% /tab %}
{% tab label="React" %}
```shell
nx g @nx/react:storybook-configuration my-react-project
```
{% /tab %}
{% tab label="Vue" %}
```shell
nx g @nx/vue:storybook-configuration my-vue-project
```
{% /tab %}
{% tab label="React Native" %}
```shell
nx g @nx/react-native:storybook-configuration my-react-native-project
```
{% /tab %}
{% /tabs %}
These framework-specific generators will also **generate stories** for you.
If you are NOT using a framework-specific generator (for [Angular](/nx-api/angular/generators/storybook-configuration), [React](/nx-api/react/generators/storybook-configuration), [React Native](/nx-api/react-native/generators/storybook-configuration), [Vue](/nx-api/vue/generators/storybook-configuration)), in the field `uiFramework` you must choose one of the following Storybook frameworks:
- `@storybook/angular`
- `@storybook/html-webpack5`
@ -82,43 +117,7 @@ Choosing one of these frameworks will have the following effects on your workspa
4. Nx will generate a new Cypress e2e app for your project (if there isn't one already) to run against the Storybook instance.
Make sure to **use the framework-specific generators** if your project is using Angular, React, Next.js or React Native: [`@nx/angular:storybook-configuration`](/nx-api/angular/generators/storybook-configuration), [`@nx/react:storybook-configuration`](/nx-api/react/generators/storybook-configuration), [`@nx/react-native:storybook-configuration`](/nx-api/react-native/generators/storybook-configuration):
{% tabs %}
{% tab label="Angular" %}
```shell
nx g @nx/angular:storybook-configuration my-angular-project
```
{% /tab %}
{% tab label="React" %}
```shell
nx g @nx/react:storybook-configuration my-react-project
```
{% /tab %}
{% tab label="React Native" %}
```shell
nx g @nx/react-native:storybook-configuration my-react-native-project
```
{% /tab %}
{% /tabs %}
These framework-specific generators will also **generate stories** for you.
### Configure your project using TypeScript
You can choose to configure your project using TypeScript instead of JavaScript. To do that, just add the `--tsConfiguration=true` flag to the above command, like this:
```shell
nx g @nx/storybook:configuration project-name --tsConfiguration=true
```
[Here is the Storybook documentation](https://storybook.js.org/docs/react/configure/overview#configure-your-project-with-typescript) if you want to learn more about configuring your project with TypeScript.
Make sure to **use the framework-specific generators** if your project is using Angular, React, Next.js or React Native: [`@nx/angular:storybook-configuration`](/nx-api/angular/generators/storybook-configuration), [`@nx/react:storybook-configuration`](/nx-api/react/generators/storybook-configuration), [`@nx/react-native:storybook-configuration`](/nx-api/react-native/generators/storybook-configuration), as shown above.
### Running Storybook
@ -157,12 +156,12 @@ The project-specific Storybook configuration is pretty much similar to what you
```text
<project root>/
├── .storybook/
│ ├── main.js
│ ├── preview.js
│ ├── tsconfig.json
│ ├── main.ts
│ └── preview.ts
├── src/
├── README.md
├── tsconfig.json
├── tsconfig.storybook.json
└── etc...
```
@ -170,7 +169,7 @@ The project-specific Storybook configuration is pretty much similar to what you
To register a [Storybook addon](https://storybook.js.org/addons/) for all Storybook instances in your workspace:
1. In your project's `.storybook/main.js` file, in the `addons` array of the `module.exports` object, add the new addon:
1. In your project's `.storybook/main.ts` file, in the `addons` array of the `module.exports` object, add the new addon:
```typescript {% fileName="<project-path>/.storybook/main.js" %}
module.exports = {
@ -180,7 +179,7 @@ To register a [Storybook addon](https://storybook.js.org/addons/) for all Storyb
};
```
2. If a decorator is required, in each project's `<project-path>/.storybook/preview.js`, you can export an array called `decorators`.
2. If a decorator is required, in each project's `<project-path>/.storybook/preview.ts`, you can export an array called `decorators`.
```typescript {% fileName="<project-path>/.storybook/preview.js" %}
import someDecorator from 'some-storybook-addon';
@ -199,6 +198,7 @@ You can find dedicated information for React and Angular:
- [Set up Storybook for Angular Projects](/recipes/storybook/overview-angular)
- [Set up Storybook for React Projects](/recipes/storybook/overview-react)
- [Set up Storybook for Vue Projects](/recipes/storybook/overview-vue)
You can find all Storybook-related Nx documentation in the [Storybook recipes section](/recipes/storybook).

View File

@ -104,7 +104,7 @@
}
},
"required": ["name", "uiFramework"],
"examplesFile": "---\ntitle: Storybook configuration generator examples\ndescription: This page contains examples for the @nx/storybook:configuration generator.\n---\n\nThis is a framework-agnostic generator for setting up Storybook configuration for a project.\n\n```bash\nnx g @nx/storybook:configuration\n```\n\nStarting Nx 16, Nx does not support Storybook v6 any more. So, Nx will configure your project to use Storybook v7. If you are not on Storybook 7 yet, please migrate. You can read more about how to migrate to Storybook 7 in our [Storybook 7 migration generator](/packages/storybook/generators/migrate-7) guide.\n\nWhen running this generator, you will be prompted to provide the following:\n\n- The `name` of the project you want to generate the configuration for.\n- The `uiFramework` you want to use. Supported values are:\n - `@storybook/angular`\n - `@storybook/html-webpack5`\n - `@storybook/nextjs`\n - `@storybook/preact-webpack5`\n - `@storybook/react-webpack5`\n - `@storybook/react-vite`\n - `@storybook/server-webpack5`\n - `@storybook/svelte-webpack5`\n - `@storybook/svelte-vite`\n - `@storybook/sveltekit`\n - `@storybook/vue-webpack5`\n - `@storybook/vue-vite`\n - `@storybook/vue3-webpack5`\n - `@storybook/vue3-vite`\n - `@storybook/web-components-webpack5`\n - `@storybook/web-components-vite`\n- Whether you want to set up [Storybook interaction tests](https://storybook.js.org/docs/angular/writing-tests/interaction-testing) (`interactionTests`). If you choose `yes`, all the necessary dependencies will be installed. Also, a `test-storybook` target will be generated in your project's `project.json`, with a command to invoke the [Storybook `test-runner`](https://storybook.js.org/docs/angular/writing-tests/test-runner). You can read more about this in the [Nx Storybook interaction tests documentation page](/packages/storybook/documents/interaction-tests).\n\nYou must provide a `name` and a `uiFramework` for the generator to work.\n\nYou can read more about how this generator works, in the [Storybook package overview page](/packages/storybook#generating-storybook-configuration).\n\nIf you are using Angular, React, React Native or Next.js in your project, it's best to use the framework specific generator:\n\n- [React Storybook Configuration Generator](/packages/react/generators/storybook-configuration) (React and Next.js projects)\n\n- [Angular Storybook Configuration Generator](/packages/angular/generators/storybook-configuration)\n\n- [React Native Storybook Configuration Generator](/packages/react-native/generators/storybook-configuration)\n\n## Examples\n\n### Generate Storybook configuration using JavaScript\n\n```bash\nnx g @nx/storybook:configuration ui --uiFramework=@storybook/web-components-vite --tsConfiguration=false\n```\n\nBy default, our generator generates TypeScript Storybook configuration files. You can choose to use JavaScript for the Storybook configuration files of your project (the files inside the `.storybook` directory, eg. `.storybook/main.js`).\n",
"examplesFile": "---\ntitle: Storybook configuration generator examples\ndescription: This page contains examples for the @nx/storybook:configuration generator.\n---\n\nThis is a framework-agnostic generator for setting up Storybook configuration for a project.\n\n```bash\nnx g @nx/storybook:configuration\n```\n\nStarting Nx 16, Nx does not support Storybook v6 any more. So, Nx will configure your project to use Storybook v7. If you are not on Storybook 7 yet, please migrate. You can read more about how to migrate to Storybook 7 in our [Storybook 7 migration generator](/packages/storybook/generators/migrate-7) guide.\n\nWhen running this generator, you will be prompted to provide the following:\n\n- The `name` of the project you want to generate the configuration for.\n- The `uiFramework` you want to use. Supported values are:\n - `@storybook/angular`\n - `@storybook/html-webpack5`\n - `@storybook/nextjs`\n - `@storybook/preact-webpack5`\n - `@storybook/react-webpack5`\n - `@storybook/react-vite`\n - `@storybook/server-webpack5`\n - `@storybook/svelte-webpack5`\n - `@storybook/svelte-vite`\n - `@storybook/sveltekit`\n - `@storybook/vue-webpack5`\n - `@storybook/vue-vite`\n - `@storybook/vue3-webpack5`\n - `@storybook/vue3-vite`\n - `@storybook/web-components-webpack5`\n - `@storybook/web-components-vite`\n- Whether you want to set up [Storybook interaction tests](https://storybook.js.org/docs/angular/writing-tests/interaction-testing) (`interactionTests`). If you choose `yes`, all the necessary dependencies will be installed. Also, a `test-storybook` target will be generated in your project's `project.json`, with a command to invoke the [Storybook `test-runner`](https://storybook.js.org/docs/angular/writing-tests/test-runner). You can read more about this in the [Nx Storybook interaction tests documentation page](/packages/storybook/documents/interaction-tests).\n\nYou must provide a `name` and a `uiFramework` for the generator to work.\n\nYou can read more about how this generator works, in the [Storybook package overview page](/packages/storybook#generating-storybook-configuration).\n\nIf you are using Angular, React, React Native or Next.js in your project, it's best to use the framework specific generator:\n\n- [React Storybook Configuration Generator](/nx-api/react/generators/storybook-configuration) (React and Next.js projects)\n\n- [Angular Storybook Configuration Generator](/nx-api/angular/generators/storybook-configuration)\n\n- [React Native Storybook Configuration Generator](/nx-api/react-native/generators/storybook-configuration)\n\n## Examples\n\n### Generate Storybook configuration using JavaScript\n\n```bash\nnx g @nx/storybook:configuration ui --uiFramework=@storybook/web-components-vite --tsConfiguration=false\n```\n\nBy default, our generator generates TypeScript Storybook configuration files. You can choose to use JavaScript for the Storybook configuration files of your project (the files inside the `.storybook` directory, eg. `.storybook/main.js`).\n",
"presets": []
},
"description": "Add Storybook configuration to a UI library or an application.",

View File

@ -656,6 +656,13 @@
"description": "This guide explains how to set up Storybook for React projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-react"
},
{
"name": "Set up Storybook for Vue Projects",
"id": "overview-vue",
"tags": ["storybook"],
"description": "This guide explains how to set up Storybook for Vue projects in your Nx workspace.",
"file": "shared/recipes/storybook/plugin-vue"
},
{
"name": "Set up Storybook for Angular Projects",
"id": "overview-angular",

View File

@ -53,7 +53,42 @@ You can generate Storybook configuration for an individual project with this com
nx g @nx/storybook:configuration project-name
```
If you are NOT using a framework-specific generator (for [Angular](/nx-api/angular/generators/storybook-configuration), [React](/nx-api/react/generators/storybook-configuration), [React Native](/nx-api/react-native/generators/storybook-configuration)), in the field `uiFramework` you must choose one of the following Storybook frameworks:
or
{% tabs %}
{% tab label="Angular" %}
```shell
nx g @nx/angular:storybook-configuration my-angular-project
```
{% /tab %}
{% tab label="React" %}
```shell
nx g @nx/react:storybook-configuration my-react-project
```
{% /tab %}
{% tab label="Vue" %}
```shell
nx g @nx/vue:storybook-configuration my-vue-project
```
{% /tab %}
{% tab label="React Native" %}
```shell
nx g @nx/react-native:storybook-configuration my-react-native-project
```
{% /tab %}
{% /tabs %}
These framework-specific generators will also **generate stories** for you.
If you are NOT using a framework-specific generator (for [Angular](/nx-api/angular/generators/storybook-configuration), [React](/nx-api/react/generators/storybook-configuration), [React Native](/nx-api/react-native/generators/storybook-configuration), [Vue](/nx-api/vue/generators/storybook-configuration)), in the field `uiFramework` you must choose one of the following Storybook frameworks:
- `@storybook/angular`
- `@storybook/html-webpack5`
@ -82,43 +117,7 @@ Choosing one of these frameworks will have the following effects on your workspa
4. Nx will generate a new Cypress e2e app for your project (if there isn't one already) to run against the Storybook instance.
Make sure to **use the framework-specific generators** if your project is using Angular, React, Next.js or React Native: [`@nx/angular:storybook-configuration`](/nx-api/angular/generators/storybook-configuration), [`@nx/react:storybook-configuration`](/nx-api/react/generators/storybook-configuration), [`@nx/react-native:storybook-configuration`](/nx-api/react-native/generators/storybook-configuration):
{% tabs %}
{% tab label="Angular" %}
```shell
nx g @nx/angular:storybook-configuration my-angular-project
```
{% /tab %}
{% tab label="React" %}
```shell
nx g @nx/react:storybook-configuration my-react-project
```
{% /tab %}
{% tab label="React Native" %}
```shell
nx g @nx/react-native:storybook-configuration my-react-native-project
```
{% /tab %}
{% /tabs %}
These framework-specific generators will also **generate stories** for you.
### Configure your project using TypeScript
You can choose to configure your project using TypeScript instead of JavaScript. To do that, just add the `--tsConfiguration=true` flag to the above command, like this:
```shell
nx g @nx/storybook:configuration project-name --tsConfiguration=true
```
[Here is the Storybook documentation](https://storybook.js.org/docs/react/configure/overview#configure-your-project-with-typescript) if you want to learn more about configuring your project with TypeScript.
Make sure to **use the framework-specific generators** if your project is using Angular, React, Next.js or React Native: [`@nx/angular:storybook-configuration`](/nx-api/angular/generators/storybook-configuration), [`@nx/react:storybook-configuration`](/nx-api/react/generators/storybook-configuration), [`@nx/react-native:storybook-configuration`](/nx-api/react-native/generators/storybook-configuration), as shown above.
### Running Storybook
@ -157,12 +156,12 @@ The project-specific Storybook configuration is pretty much similar to what you
```text
<project root>/
├── .storybook/
│ ├── main.js
│ ├── preview.js
│ ├── tsconfig.json
│ ├── main.ts
│ └── preview.ts
├── src/
├── README.md
├── tsconfig.json
├── tsconfig.storybook.json
└── etc...
```
@ -170,7 +169,7 @@ The project-specific Storybook configuration is pretty much similar to what you
To register a [Storybook addon](https://storybook.js.org/addons/) for all Storybook instances in your workspace:
1. In your project's `.storybook/main.js` file, in the `addons` array of the `module.exports` object, add the new addon:
1. In your project's `.storybook/main.ts` file, in the `addons` array of the `module.exports` object, add the new addon:
```typescript {% fileName="<project-path>/.storybook/main.js" %}
module.exports = {
@ -180,7 +179,7 @@ To register a [Storybook addon](https://storybook.js.org/addons/) for all Storyb
};
```
2. If a decorator is required, in each project's `<project-path>/.storybook/preview.js`, you can export an array called `decorators`.
2. If a decorator is required, in each project's `<project-path>/.storybook/preview.ts`, you can export an array called `decorators`.
```typescript {% fileName="<project-path>/.storybook/preview.js" %}
import someDecorator from 'some-storybook-addon';
@ -199,6 +198,7 @@ You can find dedicated information for React and Angular:
- [Set up Storybook for Angular Projects](/recipes/storybook/overview-angular)
- [Set up Storybook for React Projects](/recipes/storybook/overview-react)
- [Set up Storybook for Vue Projects](/recipes/storybook/overview-vue)
You can find all Storybook-related Nx documentation in the [Storybook recipes section](/recipes/storybook).

View File

@ -0,0 +1,80 @@
---
title: Set up Storybook for Vue Projects
description: This guide explains how to set up Storybook for Vue projects in your Nx workspace.
---
# Set up Storybook for Vue Projects
This guide will walk you through setting up [Storybook](https://storybook.js.org) for Vue projects in your Nx workspace.
{% callout type="warning" title="Set up Storybook in your workspace" %}
You first need to set up Storybook for your Nx workspace, if you haven't already. You can read the [Storybook plugin overview guide](/nx-api/storybook) to get started.
{% /callout %}
## Generate Storybook Configuration for a Vue project
You can generate Storybook configuration for an individual Vue project by using the [`@nx/vue:storybook-configuration` generator](/nx-api/vue/generators/storybook-configuration), like this:
```shell
nx g @nx/vue:storybook-configuration project-name
```
## Auto-generate Stories
The [`@nx/vue:storybook-configuration` generator](/nx-api/vue/generators/storybook-configuration) has the option to automatically generate `*.stories.ts` files for each component declared in the library. The stories will be generated using [Component Story Format 3 (CSF3)](https://storybook.js.org/blog/storybook-csf3-is-here/).
```text
<some-folder>/
├── MyComponent.vue
└── MyComponent.stories.ts
```
If you add more components to your project, and want to generate stories for all your (new) components at any point, you can use the [`@nx/vue:stories` generator](/nx-api/vue/generators/stories):
```shell
nx g @nx/vue:stories --project=<project-name>
```
{% callout type="note" title="Example" %}
Let's take for a example a library in your workspace, under `libs/feature/ui`, called `feature-ui`. This library contains a component, called `my-button`.
The command to generate stories for that library would be:
```shell
nx g @nx/vue:stories --project=feature-ui
```
and the result would be the following:
```text
<workspace name>/
├── apps/
├── libs/
│ ├── feature/
│ │ ├── ui/
| | | ├── .storybook/
| | | ├── src/
| | | | ├──lib
| | | | | ├──my-button
| | | | | | ├── MyButton.vue
| | | | | | ├── MyButton.stories.ts
| | | | | | └── etc...
| | | | | └── etc...
| | | ├── README.md
| | | ├── tsconfig.json
| | | └── etc...
| | └── etc...
| └── etc...
├── nx.json
├── package.json
├── README.md
└── etc...
```
{% /callout %}
## More Documentation
You can find all Storybook-related Nx topics [here](/nx-api#storybook).
For more on using Storybook, see the [official Storybook documentation](https://storybook.js.org/docs/vue/get-started/introduction).

View File

@ -112,6 +112,7 @@
- [Wait for Tasks to Finish](/recipes/node/wait-for-tasks)
- [Storybook](/recipes/storybook)
- [Set up Storybook for React Projects](/recipes/storybook/overview-react)
- [Set up Storybook for Vue Projects](/recipes/storybook/overview-vue)
- [Set up Storybook for Angular Projects](/recipes/storybook/overview-angular)
- [Configuring Storybook on Nx](/recipes/storybook/configuring-storybook)
- [One main Storybook instance for all projects](/recipes/storybook/one-storybook-for-all)

View File

@ -0,0 +1,36 @@
import {
checkFilesExist,
cleanupProject,
newProject,
runCLI,
setMaxWorkers,
uniq,
} from '@nx/e2e/utils';
import { join } from 'path';
describe('Storybook generators and executors for Vue projects', () => {
const vueStorybookApp = uniq('vue-app');
let proj;
beforeAll(async () => {
proj = newProject();
runCLI(
`generate @nx/vue:app ${vueStorybookApp} --project-name-and-root-format=as-provided --no-interactive`
);
setMaxWorkers(join(vueStorybookApp, 'project.json'));
runCLI(
`generate @nx/vue:storybook-configuration ${vueStorybookApp} --generateStories --no-interactive`
);
});
afterAll(() => {
cleanupProject();
});
describe('build storybook', () => {
it('should build a vue based storybook setup', () => {
// build
runCLI(`run ${vueStorybookApp}:build-storybook --verbose`);
checkFilesExist(`dist/storybook/${vueStorybookApp}/index.html`);
}, 300_000);
});
});

View File

@ -39,11 +39,11 @@ You can read more about how this generator works, in the [Storybook package over
If you are using Angular, React, React Native or Next.js in your project, it's best to use the framework specific generator:
- [React Storybook Configuration Generator](/packages/react/generators/storybook-configuration) (React and Next.js projects)
- [React Storybook Configuration Generator](/nx-api/react/generators/storybook-configuration) (React and Next.js projects)
- [Angular Storybook Configuration Generator](/packages/angular/generators/storybook-configuration)
- [Angular Storybook Configuration Generator](/nx-api/angular/generators/storybook-configuration)
- [React Native Storybook Configuration Generator](/packages/react-native/generators/storybook-configuration)
- [React Native Storybook Configuration Generator](/nx-api/react-native/generators/storybook-configuration)
## Examples

View File

@ -35,12 +35,7 @@ import {
pleaseUpgrade,
storybookMajorVersion,
} from '../../utils/utilities';
import {
coreJsVersion,
nxVersion,
storybookVersion,
tsNodeVersion,
} from '../../utils/versions';
import { coreJsVersion, nxVersion, tsNodeVersion } from '../../utils/versions';
import { interactionTestsDependencies } from './lib/interaction-testing.utils';
export async function configurationGenerator(

View File

@ -549,13 +549,17 @@ export function createProjectStorybookDir(
usesVite?: boolean,
viteConfigFilePath?: string
) {
const projectDirectory =
let projectDirectory =
projectType === 'application'
? isNextJs
? 'components'
: 'src/app'
: 'src/lib';
if (uiFramework === '@storybook/vue3-vite') {
projectDirectory = 'src/components';
}
const storybookConfigExists = projectIsRootProjectInStandaloneWorkspace
? tree.exists('.storybook/main.js') || tree.exists('.storybook/main.ts')
: tree.exists(join(root, '.storybook/main.ts')) ||

View File

@ -83,6 +83,15 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
}
}
if (schema.uiFramework === '@storybook/vue3-vite') {
if (
!packageJson.dependencies['@storybook/vue3'] &&
!packageJson.devDependencies['@storybook/vue3']
) {
devDependencies['@storybook/vue3'] = storybook7VersionToInstall;
}
}
if (schema.uiFramework === '@storybook/angular') {
if (
!packageJson.dependencies['@angular/forms'] &&

View File

@ -33,7 +33,8 @@
"nx",
"typescript",
"@nx/cypress",
"@nx/playwright"
"@nx/playwright",
"@nx/storybook"
]
}
]

View File

@ -0,0 +1,38 @@
This generator will generate stories for all your components in your project. The stories will be generated using [Component Story Format 3 (CSF3)](https://storybook.js.org/blog/storybook-csf3-is-here/).
```bash
nx g @nx/vue:stories project-name
```
You can read more about how this generator works, in the [Storybook for Vue overview page](/recipes/storybook/overview-vue#auto-generate-stories).
When running this generator, you will be prompted to provide the following:
- The `name` of the project you want to generate the configuration for.
- Whether you want to set up [Storybook interaction tests](https://storybook.js.org/docs/angular/writing-tests/interaction-testing) (`interactionTests`). If you choose `yes`, a `play` function will be added to your stories, and all the necessary dependencies will be installed. You can read more about this in the [Nx Storybook interaction tests documentation page](/packages/storybook/documents/interaction-tests)..
You must provide a `name` for the generator to work.
By default, this generator will also set up [Storybook interaction tests](https://storybook.js.org/docs/angular/writing-tests/interaction-testing). If you don't want to set up Storybook interaction tests, you can pass the `--interactionTests=false` option, but it's not recommended.
There are a number of other options available. Let's take a look at some examples.
## Examples
### Ignore certain paths when generating stories
```bash
nx g @nx/vue:stories --name=ui --ignorePaths=libs/ui/src/not-stories/**,**/**/src/**/*.other.*
```
This will generate stories for all the components in the `ui` project, except for the ones in the `libs/ui/src/not-stories` directory, and also for components that their file name is of the pattern `*.other.*`.
This is useful if you have a project that contains components that are not meant to be used in isolation, but rather as part of a larger component.
### Generate stories using JavaScript instead of TypeScript
```bash
nx g @nx/vue:stories --name=ui --js=true
```
This will generate stories for all the components in the `ui` project using JavaScript instead of TypeScript. So, you will have `.stories.js` files next to your components.

View File

@ -0,0 +1,55 @@
This generator will set up Storybook for your **Vue** project. You can also use this generator to generate Storybook configuration for your **Next.js** project. By default, starting Nx 16, Storybook v7 is used.
```bash
nx g @nx/vue:storybook-configuration project-name
```
You can read more about how this generator works, in the [Storybook for Vue overview page](/recipes/storybook/overview-vue#generate-storybook-configuration-for-a-vue-project).
When running this generator, you will be prompted to provide the following:
- The `name` of the project you want to generate the configuration for.
- Whether you want to set up [Storybook interaction tests](https://storybook.js.org/docs/vue/writing-tests/interaction-testing) (`interactionTests`). If you choose `yes`, a `play` function will be added to your stories, and all the necessary dependencies will be installed. Also, a `test-storybook` target will be generated in your project's `project.json`, with a command to invoke the [Storybook `test-runner`](https://storybook.js.org/docs/vue/writing-tests/test-runner). You can read more about this in the [Nx Storybook interaction tests documentation page](/packages/storybook/documents/interaction-tests)..
- Whether you want to `generateStories` for the components in your project. If you choose `yes`, a `.stories.ts` file will be generated next to each of your components in your project.
You must provide a `name` for the generator to work.
By default, this generator will also set up [Storybook interaction tests](https://storybook.js.org/docs/vue/writing-tests/interaction-testing). If you don't want to set up Storybook interaction tests, you can pass the `--interactionTests=false` option, but it's not recommended.
There are a number of other options available. Let's take a look at some examples.
## Examples
### Generate Storybook configuration
```bash
nx g @nx/vue:storybook-configuration ui
```
This will generate Storybook configuration for the `ui` project using TypeScript for the Storybook configuration files (the files inside the `.storybook` directory, eg. `.storybook/main.ts`).
### Ignore certain paths when generating stories
```bash
nx g @nx/vue:storybook-configuration ui --generateStories=true --ignorePaths=libs/ui/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts
```
This will generate a Storybook configuration for the `ui` project and generate stories for all components in the `libs/ui/src/lib` directory, except for the ones in the `libs/ui/src/not-stories` directory, and the ones in the `apps/my-app` directory that end with `.something.ts`, and also for components that their file name is of the pattern `*.other.*`.
This is useful if you have a project that contains components that are not meant to be used in isolation, but rather as part of a larger component.
### Generate stories using JavaScript instead of TypeScript
```bash
nx g @nx/vue:storybook-configuration ui --generateStories=true --js=true
```
This will generate stories for all the components in the `ui` project using JavaScript instead of TypeScript. So, you will have `.stories.js` files next to your components.
### Generate Storybook configuration using JavaScript
```bash
nx g @nx/vue:storybook-configuration ui --tsConfiguration=false
```
By default, our generator generates TypeScript Storybook configuration files. You can choose to use JavaScript for the Storybook configuration files of your project (the files inside the `.storybook` directory, eg. `.storybook/main.js`).

View File

@ -33,6 +33,18 @@
"factory": "./src/generators/setup-tailwind/setup-tailwind",
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Set up Tailwind configuration for a project."
},
"storybook-configuration": {
"factory": "./src/generators/storybook-configuration/configuration",
"schema": "./src/generators/storybook-configuration/schema.json",
"description": "Set up storybook for a Vue app or library.",
"hidden": false
},
"stories": {
"factory": "./src/generators/stories/stories",
"schema": "./src/generators/stories/schema.json",
"description": "Create stories for all components declared in an app or library.",
"hidden": false
}
}
}

View File

@ -28,6 +28,7 @@
"migrations": "./migrations.json"
},
"dependencies": {
"minimatch": "3.0.5",
"tslib": "^2.3.0",
"@nx/devkit": "file:../devkit",
"@nx/jest": "file:../jest",

View File

@ -138,27 +138,6 @@ describe('component', () => {
});
});
// TODO: figure out routing
xdescribe('--routing', () => {
it('should add routes to the component', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
routing: true,
});
const content = appTree
.read('my-lib/src/components/hello/hello.tsx')
.toString();
expect(content).toContain('react-router-dom');
expect(content).toMatch(/<Route\s*path="\/"/);
expect(content).toMatch(/<Link\s*to="\/"/);
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['react-router-dom']).toBeDefined();
});
});
describe('--directory', () => {
it('should create component under the directory', async () => {
await componentGenerator(appTree, {

View File

@ -114,7 +114,7 @@ async function normalizeOptions(
const { sourceRoot: projectSourceRoot, projectType } = project;
const directory = await getDirectory(host, options);
const directory = await getDirectory(options);
if (options.export && projectType === 'application') {
logger.warn(
@ -134,7 +134,7 @@ async function normalizeOptions(
};
}
async function getDirectory(host: Tree, options: Schema) {
async function getDirectory(options: Schema) {
if (options.directory) return options.directory;
if (options.flat) return 'components';
const { className, fileName } = names(options.name);

View File

@ -0,0 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vue:stories for applications should create the stories with interaction tests 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import NxWelcome from './NxWelcome.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof NxWelcome> = {
component: NxWelcome,
title: 'NxWelcome',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {},
};
export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy();
},
};
"
`;
exports[`vue:stories for applications should create the stories with interaction tests 2`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import anotherCmp from './another-cmp.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof anotherCmp> = {
component: anotherCmp,
title: 'anotherCmp',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
};
export const Heading: Story = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to anotherCmp!/gi)).toBeTruthy();
},
};
"
`;
exports[`vue:stories for applications should create the stories without interaction tests 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import NxWelcome from './NxWelcome.vue';
const meta: Meta<typeof NxWelcome> = {
component: NxWelcome,
title: 'NxWelcome',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {},
};
"
`;
exports[`vue:stories for applications should create the stories without interaction tests 2`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import anotherCmp from './another-cmp.vue';
const meta: Meta<typeof anotherCmp> = {
component: anotherCmp,
title: 'anotherCmp',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
};
"
`;
exports[`vue:stories for applications should not update existing stories 1`] = `
"import { ComponentStory, ComponentMeta } from '@storybook/vue3';
"
`;

View File

@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vue:stories for libraries should create the stories with interaction tests 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import testUiLib from './test-ui-lib.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof testUiLib> = {
component: testUiLib,
title: 'testUiLib',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {},
};
export const Heading: Story = {
args: {},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to testUiLib!/gi)).toBeTruthy();
},
};
"
`;
exports[`vue:stories for libraries should create the stories with interaction tests 2`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import anotherCmp from './another-cmp.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof anotherCmp> = {
component: anotherCmp,
title: 'anotherCmp',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
};
export const Heading: Story = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to anotherCmp!/gi)).toBeTruthy();
},
};
"
`;
exports[`vue:stories for libraries should create the stories without interaction tests 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import testUiLib from './test-ui-lib.vue';
const meta: Meta<typeof testUiLib> = {
component: testUiLib,
title: 'testUiLib',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {},
};
"
`;
exports[`vue:stories for libraries should create the stories without interaction tests 2`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import anotherCmp from './another-cmp.vue';
const meta: Meta<typeof anotherCmp> = {
component: anotherCmp,
title: 'anotherCmp',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
};
"
`;

View File

@ -0,0 +1,115 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vue:component-story default setup component with other syntax of props defined should create a story with controls 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import testUiLib from './test-ui-lib.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof testUiLib> = {
component: testUiLib,
title: 'testUiLib',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
};
export const Heading: Story = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to testUiLib!/gi)).toBeTruthy();
},
};
"
`;
exports[`vue:component-story default setup component with props defined should create a story with controls 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import testUiLib from './test-ui-lib.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof testUiLib> = {
component: testUiLib,
title: 'testUiLib',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
};
export const Heading: Story = {
args: {
name: 'name',
displayAge: false,
age: 0,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to testUiLib!/gi)).toBeTruthy();
},
};
"
`;
exports[`vue:component-story default setup default component setup should properly set up the story 1`] = `
"import type { Meta, StoryObj } from '@storybook/vue3';
import testUiLib from './test-ui-lib.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof testUiLib> = {
component: testUiLib,
title: 'testUiLib',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {
},
};
export const Heading: Story = {
args: {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to testUiLib!/gi)).toBeTruthy();
},
};
"
`;

View File

@ -0,0 +1,141 @@
import { getProjects, Tree, updateProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import libraryGenerator from '../../library/library';
import { createComponentStories } from './component-story';
import { Linter } from '@nx/linter';
describe('vue:component-story', () => {
let appTree: Tree;
let cmpPath = 'test-ui-lib/src/components/test-ui-lib.vue';
let storyFilePath = 'test-ui-lib/src/components/test-ui-lib.stories.ts';
describe('default setup', () => {
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib');
});
describe('default component setup', () => {
beforeEach(async () => {
createComponentStories(
appTree,
{
interactionTests: true,
project: 'test-ui-lib',
},
'components/test-ui-lib.vue'
);
});
it('should properly set up the story', () => {
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
describe('component with props defined', () => {
beforeEach(async () => {
appTree.write(
cmpPath,
`<script setup lang="ts">
defineProps<{
name: string;
displayAge: boolean;
age: number;
}>();
</script>
<template>
<div>
<p>Welcome to Vlv!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
`
);
createComponentStories(
appTree,
{
interactionTests: true,
project: 'test-ui-lib',
},
'components/test-ui-lib.vue'
);
});
it('should create a story with controls', () => {
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
describe('component with other syntax of props defined', () => {
beforeEach(async () => {
appTree.write(
cmpPath,
`<script>
export default {
name: 'HelloWorld',
props: {
name: string;
displayAge: boolean;
age: number;
}
}
</script>
<template>
<div>
<p>Welcome to Vlv!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
`
);
createComponentStories(
appTree,
{
interactionTests: true,
project: 'test-ui-lib',
},
'components/test-ui-lib.vue'
);
});
it('should create a story with controls', () => {
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
});
});
});
});
export async function createTestUILib(libName: string): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
name: libName,
linter: Linter.EsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
unitTestRunner: 'jest',
projectNameAndRootFormat: 'as-provided',
});
const currentWorkspaceJson = getProjects(appTree);
const projectConfig = currentWorkspaceJson.get(libName);
projectConfig.targets.lint.options.linter = 'eslint';
updateProjectConfiguration(appTree, libName, projectConfig);
return appTree;
}

View File

@ -0,0 +1,60 @@
import {
generateFiles,
getProjects,
joinPathFragments,
normalizePath,
Tree,
} from '@nx/devkit';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { StorybookStoriesSchema } from '../stories';
import {
camelCase,
createDefautPropsObject,
getDefinePropsObject,
} from './utils';
let tsModule: typeof import('typescript');
export function createComponentStories(
host: Tree,
{ project, js, interactionTests }: StorybookStoriesSchema,
componentPath: string
) {
if (!tsModule) {
tsModule = ensureTypescript();
}
const proj = getProjects(host).get(project);
const sourceRoot = proj.sourceRoot;
const componentFilePath = joinPathFragments(sourceRoot, componentPath);
const componentDirectory = componentFilePath.replace(
componentFilePath.slice(componentFilePath.lastIndexOf('/')),
''
);
const componentFileName = componentFilePath
.slice(componentFilePath.lastIndexOf('/') + 1)
.replace('.vue', '');
const name = componentFileName;
const contents = host.read(componentFilePath, 'utf-8');
const propsObject = getDefinePropsObject(contents);
generateFiles(
host,
joinPathFragments(__dirname, `./files${js ? '/js' : '/ts'}`),
normalizePath(componentDirectory),
{
tmpl: '',
componentFileName: name,
componentImportFileName: `${name}.vue`,
props: createDefautPropsObject(propsObject),
componentName: camelCase(name),
interactionTests,
}
);
if (contents === null) {
throw new Error(`Failed to read ${componentFilePath}`);
}
}

View File

@ -0,0 +1,25 @@
import componentName from './<%= componentImportFileName %>';
<% if ( interactionTests ) { %>
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
<% } %>
export default {
component: <%= componentName %>,
title: '<%= componentName %>'
};
export const Primary = {
args: {<% for (let prop of props) { %>
<%= prop.name %>: <%- prop.defaultValue %>,<% } %>
},
};
<% if ( interactionTests ) { %>
export const Heading: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy();
},
};
<% } %>

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import <%= componentName %> from './<%= componentImportFileName %>';
<% if ( interactionTests ) { %>
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
<% } %>
const meta: Meta<typeof <%= componentName %>> = {
component: <%= componentName %>,
title: '<%= componentName %>',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: {<% for (let prop of props) { %>
<%= prop.name %>: <%- prop.defaultValue %>,<% } %>
},
};
<% if ( interactionTests ) { %>
export const Heading: Story = {
args: {<% for (let prop of props) { %>
<%= prop.name %>: <%- prop.defaultValue %>,<% } %>
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(/Welcome to <%=componentName%>!/gi)).toBeTruthy();
},
};
<% } %>

View File

@ -0,0 +1,76 @@
export function camelCase(input: string): string {
if (input.indexOf('-') > 1) {
return input
.toLowerCase()
.replace(/-(.)/g, (_match, group1) => group1.toUpperCase())
.replace('.', '');
} else {
return input;
}
}
export function createDefautPropsObject(propsObject: {
[key: string]: string;
}): {
name: string;
defaultValue: any;
}[] {
const props = [];
for (const key in propsObject) {
if (Object.prototype.hasOwnProperty.call(propsObject, key)) {
let defaultValueOfProp;
const element = propsObject[key];
if (element === 'string') {
defaultValueOfProp = `'${key}'`;
} else if (element === 'boolean') {
defaultValueOfProp = false;
} else if (element === 'number') {
defaultValueOfProp = 0;
}
props.push({
name: key,
defaultValue: defaultValueOfProp,
});
}
}
return props;
}
export function getDefinePropsObject(vueComponentFileContent: string): {
[key: string]: string;
} {
const scriptTagRegex = /<script[^>]*>([\s\S]*?)<\/script>/;
const match = vueComponentFileContent?.match(scriptTagRegex);
let propsContent;
if (match && match[1]) {
const scriptContent = match[1].trim();
const definePropsRegex = /defineProps<([\s\S]*?)>/;
const definePropsMatch = scriptContent.match(definePropsRegex);
if (definePropsMatch && definePropsMatch[1]) {
propsContent = definePropsMatch[1].trim();
} else {
const propsRegex = /(props:\s*\{[\s\S]*?\})/;
const match = scriptContent.match(propsRegex);
if (match && match[1]) {
propsContent = match[1].trim();
} else {
// No props found
}
}
} else {
// No props found
}
const attributes = {};
if (propsContent) {
const keyTypeRegex = /(\w+):\s*(\w+);/g;
let match;
while ((match = keyTypeRegex.exec(propsContent)) !== null) {
attributes[match[1]] = match[2];
}
}
return attributes;
}

View File

@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxVueStorybookStories",
"title": "Generate Vue Storybook stories",
"description": "Generate stories/specs for all components declared in a project.",
"type": "object",
"properties": {
"project": {
"type": "string",
"aliases": ["name", "projectName"],
"description": "Project for which to generate stories.",
"$default": {
"$source": "projectName",
"index": 0
},
"x-prompt": "For which project do you want to generate stories?",
"x-priority": "important"
},
"generateCypressSpecs": {
"type": "boolean",
"description": "Automatically generate `*.spec.ts` files in the cypress e2e app generated by the cypress-configure generator."
},
"cypressProject": {
"type": "string",
"description": "The Cypress project to generate the stories under. This is inferred from `project` by default."
},
"interactionTests": {
"type": "boolean",
"description": "Set up Storybook interaction tests.",
"x-prompt": "Do you want to set up Storybook interaction tests?",
"x-priority": "important",
"default": true
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"ignorePaths": {
"type": "array",
"description": "Paths to ignore when looking for components.",
"items": {
"type": "string",
"description": "Path to ignore."
},
"examples": [
"apps/my-app/src/not-stories/**",
"**/**/src/**/not-stories/**",
"libs/my-lib/**/*.something.ts",
"**/**/src/**/*.other.*",
"libs/my-lib/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts"
]
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
}
},
"required": ["project"],
"examplesFile": "../../../docs/stories-examples.md"
}

View File

@ -0,0 +1,272 @@
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import applicationGenerator from '../application/application';
import storiesGenerator from './stories';
const componentContent = `<script setup lang="ts">
defineProps<{
name: string;
displayAge: boolean;
age: number;
}>();
</script>
<template>
<div>
<p>Welcome to Vlv!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
`;
describe('vue:stories for applications', () => {
let appTree: Tree;
beforeEach(async () => {
appTree = await createTestUIApp('test-ui-app');
// create another component
appTree.write(
'test-ui-app/src/components/another-cmp/another-cmp.vue',
componentContent
);
});
it('should create the stories with interaction tests', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
});
expect(
appTree.read('test-ui-app/src/components/NxWelcome.stories.ts', 'utf-8')
).toMatchSnapshot();
expect(
appTree.read(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts',
'utf-8'
)
).toMatchSnapshot();
});
it('should create the stories without interaction tests', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
interactionTests: false,
});
expect(
appTree.read('test-ui-app/src/components/NxWelcome.stories.ts', 'utf-8')
).toMatchSnapshot();
expect(
appTree.read(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts',
'utf-8'
)
).toMatchSnapshot();
});
it('should not update existing stories', async () => {
appTree.write(
'test-ui-app/src/components/NxWelcome.stories.ts',
`import { ComponentStory, ComponentMeta } from '@storybook/vue3'`
);
await storiesGenerator(appTree, {
project: 'test-ui-app',
});
expect(
appTree.read('test-ui-app/src/components/NxWelcome.stories.ts', 'utf-8')
).toMatchSnapshot();
});
describe('ignore paths', () => {
beforeEach(() => {
appTree.write(
'test-ui-app/src/components/test-path/ignore-it/another-one.vue',
componentContent
);
appTree.write(
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.vue',
componentContent
);
});
it('should generate stories for all if no ignorePaths', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
});
expect(
appTree.exists('test-ui-app/src/components/NxWelcome.stories.ts')
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.stories.ts'
)
).toBeTruthy();
});
it('should ignore entire paths', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
ignorePaths: [
`test-ui-app/src/components/another-cmp/**`,
`**/**/src/**/test-path/ignore-it/**`,
],
});
expect(
appTree.exists('test-ui-app/src/components/NxWelcome.stories.ts')
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeFalsy();
expect(
appTree.exists(
'test-ui-app/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeFalsy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.stories.ts'
)
).toBeFalsy();
});
it('should ignore path or a pattern', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
ignorePaths: [
'test-ui-app/src/components/another-cmp/**/*.skip.*',
'**/**/src/**/test-path/**',
],
});
expect(
appTree.exists('test-ui-app/src/components/NxWelcome.stories.ts')
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeFalsy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.stories.ts'
)
).toBeFalsy();
});
it('should ignore direct path to component', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
ignorePaths: ['test-ui-app/src/components/another-cmp/**/*.skip.vue'],
});
expect(
appTree.exists('test-ui-app/src/components/NxWelcome.stories.ts')
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.stories.ts'
)
).toBeFalsy();
});
it('should ignore a path that has a nested component, but still generate nested component stories', async () => {
appTree.write(
'test-ui-app/src/components/another-cmp/comp-a/comp-a.vue',
componentContent
);
await storiesGenerator(appTree, {
project: 'test-ui-app',
ignorePaths: [
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.vue',
],
});
expect(
appTree.exists('test-ui-app/src/components/NxWelcome.stories.ts')
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/comp-a/comp-a.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-app/src/components/another-cmp/another-cmp-test.skip.stories.ts'
)
).toBeFalsy();
});
});
});
export async function createTestUIApp(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await applicationGenerator(appTree, {
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
style: 'css',
unitTestRunner: 'none',
name: libName,
js: plainJS,
projectNameAndRootFormat: 'as-provided',
});
return appTree;
}

View File

@ -0,0 +1,201 @@
import { Tree } from '@nx/devkit';
import storiesGenerator from './stories';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import libraryGenerator from '../library/library';
const componentContent = `<script setup lang="ts">
defineProps<{
name: string;
displayAge: boolean;
age: number;
}>();
</script>
<template>
<div>
<p>Welcome to Vlv!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
`;
describe('vue:stories for libraries', () => {
let appTree: Tree;
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib');
appTree.write(
'test-ui-lib/src/components/another-cmp/another-cmp.vue',
componentContent
);
});
it('should create the stories with interaction tests', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-lib',
});
expect(
appTree.read('test-ui-lib/src/components/test-ui-lib.stories.ts', 'utf-8')
).toMatchSnapshot();
expect(
appTree.read(
'test-ui-lib/src/components/another-cmp/another-cmp.stories.ts',
'utf-8'
)
).toMatchSnapshot();
const packageJson = JSON.parse(appTree.read('package.json', 'utf-8'));
expect(
packageJson.devDependencies['@storybook/addon-interactions']
).toBeDefined();
expect(packageJson.devDependencies['@storybook/test-runner']).toBeDefined();
expect(
packageJson.devDependencies['@storybook/testing-library']
).toBeDefined();
});
it('should create the stories without interaction tests', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-lib',
interactionTests: false,
});
expect(
appTree.read('test-ui-lib/src/components/test-ui-lib.stories.ts', 'utf-8')
).toMatchSnapshot();
expect(
appTree.read(
'test-ui-lib/src/components/another-cmp/another-cmp.stories.ts',
'utf-8'
)
).toMatchSnapshot();
const packageJson = JSON.parse(appTree.read('package.json', 'utf-8'));
expect(
packageJson.devDependencies['@storybook/addon-interactions']
).toBeUndefined();
expect(
packageJson.devDependencies['@storybook/test-runner']
).toBeUndefined();
expect(
packageJson.devDependencies['@storybook/testing-library']
).toBeUndefined();
});
describe('ignore paths', () => {
beforeEach(() => {
appTree.write(
'test-ui-lib/src/components/test-path/ignore-it/another-one.vue',
componentContent
);
appTree.write(
'test-ui-lib/src/components/another-cmp/another-cmp.skip.vue',
componentContent
);
});
it('should generate stories for all if no ignorePaths', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-lib',
});
expect(
appTree.exists(
'test-ui-lib/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-lib/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-lib/src/components/another-cmp/another-cmp.skip.stories.ts'
)
).toBeTruthy();
});
it('should ignore entire paths', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-lib',
ignorePaths: [
'test-ui-lib/src/components/another-cmp/**',
'**/**/src/**/test-path/ignore-it/**',
],
});
expect(
appTree.exists(
'test-ui-lib/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeFalsy();
expect(
appTree.exists(
'test-ui-lib/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeFalsy();
expect(
appTree.exists(
'test-ui-lib/src/components/another-cmp/another-cmp.skip.stories.ts'
)
).toBeFalsy();
});
it('should ignore path or a pattern', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-lib',
ignorePaths: [
'test-ui-lib/src/components/another-cmp/**/*.skip.*',
'**/test-ui-lib/src/**/test-path/**',
],
});
expect(
appTree.exists(
'test-ui-lib/src/components/another-cmp/another-cmp.stories.ts'
)
).toBeTruthy();
expect(
appTree.exists(
'test-ui-lib/src/components/test-path/ignore-it/another-one.stories.ts'
)
).toBeFalsy();
expect(
appTree.exists(
'test-ui-lib/src/components/another-cmp/another-cmp.skip.stories.ts'
)
).toBeFalsy();
});
});
});
export async function createTestUILib(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
linter: Linter.EsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
unitTestRunner: 'none',
name: libName,
projectNameAndRootFormat: 'as-provided',
});
return appTree;
}

View File

@ -0,0 +1,106 @@
import {
addDependenciesToPackageJson,
convertNxGenerator,
ensurePackage,
formatFiles,
GeneratorCallback,
getProjects,
joinPathFragments,
ProjectConfiguration,
runTasksInSerial,
Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { basename, join } from 'path';
import minimatch = require('minimatch');
import { nxVersion } from '../../utils/versions';
import { createComponentStories } from './lib/component-story';
export interface StorybookStoriesSchema {
project: string;
interactionTests?: boolean;
js?: boolean;
ignorePaths?: string[];
skipFormat?: boolean;
cypressProject?: string;
generateCypressSpecs?: boolean;
}
export async function createAllStories(
tree: Tree,
projectName: string,
interactionTests: boolean,
js: boolean,
projectConfiguration: ProjectConfiguration,
ignorePaths?: string[]
) {
const { sourceRoot, root } = projectConfiguration;
let componentPaths: string[] = [];
const projectPath = joinPathFragments(sourceRoot, 'components');
visitNotIgnoredFiles(tree, projectPath, (path) => {
// Ignore private files starting with "_".
if (basename(path).startsWith('_')) return;
if (ignorePaths?.some((pattern) => minimatch(path, pattern))) return;
if (path.endsWith('.vue')) {
// Let's see if the .stories.* file exists
const ext = path.slice(path.lastIndexOf('.'));
const storyPathJs = `${path.split(ext)[0]}.stories.js`;
const storyPathTs = `${path.split(ext)[0]}.stories.ts`;
if (!tree.exists(storyPathJs) && !tree.exists(storyPathTs)) {
componentPaths.push(path);
}
}
});
await Promise.all(
componentPaths.map(async (componentPath) => {
const relativeCmpDir = componentPath.replace(join(sourceRoot, '/'), '');
createComponentStories(
tree,
{
project: projectName,
interactionTests,
js,
},
relativeCmpDir
);
})
);
}
export async function storiesGenerator(
host: Tree,
schema: StorybookStoriesSchema
) {
const projects = getProjects(host);
const projectConfiguration = projects.get(schema.project);
schema.interactionTests = schema.interactionTests ?? true;
await createAllStories(
host,
schema.project,
schema.interactionTests,
schema.js,
projectConfiguration,
schema.ignorePaths
);
const tasks: GeneratorCallback[] = [];
if (schema.interactionTests) {
const { interactionTestsDependencies, addInteractionsInAddons } =
ensurePackage<typeof import('@nx/storybook')>('@nx/storybook', nxVersion);
tasks.push(
addDependenciesToPackageJson(host, {}, interactionTestsDependencies())
);
addInteractionsInAddons(host, projectConfiguration);
}
if (!schema.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(...tasks);
}
export default storiesGenerator;
export const storiesSchematic = convertNxGenerator(storiesGenerator);

View File

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vue:storybook-configuration should configure everything and install correct dependencies 1`] = `
"import type { StorybookConfig } from '@storybook/vue3-vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { mergeConfig } from 'vite';
const config: StorybookConfig = {
stories: ['../src/components/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
viteFinal: async (config) =>
mergeConfig(config, {
plugins: [nxViteTsPaths()],
}),
};
export default config;
// To customize your Vite configuration you can use the viteFinal field.
// Check https://storybook.js.org/docs/react/builders/vite#configuration
// and https://nx.dev/recipes/storybook/custom-builder-configs
"
`;
exports[`vue:storybook-configuration should generate stories for components 1`] = `null`;
exports[`vue:storybook-configuration should generate stories for components without interaction tests 1`] = `null`;

View File

@ -0,0 +1,165 @@
import { logger, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import applicationGenerator from '../application/application';
import componentGenerator from '../component/component';
import libraryGenerator from '../library/library';
import storybookConfigurationGenerator from './configuration';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
const componentContent = `<script setup lang="ts">
defineProps<{
name: string;
displayAge: boolean;
age: number;
}>();
</script>
<template>
<div>
<p>Welcome to Vlv!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
`;
describe('vue:storybook-configuration', () => {
let appTree;
beforeEach(async () => {
jest.spyOn(logger, 'warn').mockImplementation(() => {});
jest.spyOn(logger, 'debug').mockImplementation(() => {});
jest.resetModules();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should configure everything and install correct dependencies', async () => {
appTree = await createTestUILib('test-ui-lib');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-lib',
});
expect(
appTree.read('test-ui-lib/.storybook/main.ts', 'utf-8')
).toMatchSnapshot();
expect(appTree.exists('test-ui-lib/tsconfig.storybook.json')).toBeTruthy();
const packageJson = JSON.parse(appTree.read('package.json', 'utf-8'));
expect(packageJson.devDependencies['@storybook/vue3-vite']).toBeDefined();
expect(packageJson.devDependencies['@storybook/vue3']).toBeDefined();
expect(
packageJson.devDependencies['@storybook/addon-interactions']
).toBeDefined();
expect(packageJson.devDependencies['@storybook/test-runner']).toBeDefined();
expect(
packageJson.devDependencies['@storybook/testing-library']
).toBeDefined();
});
it('should generate stories for components', async () => {
appTree = await createTestUILib('test-ui-lib');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-lib',
generateStories: true,
});
expect(
appTree.exists('test-ui-lib/src/components/test-ui-lib.stories.ts')
).toBeTruthy();
});
it('should configure everything at once', async () => {
appTree = await createTestAppLib('test-ui-app');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-app',
});
expect(appTree.exists('test-ui-app/.storybook/main.ts')).toBeTruthy();
expect(appTree.exists('test-ui-app/tsconfig.storybook.json')).toBeTruthy();
});
it('should generate stories for components', async () => {
appTree = await createTestAppLib('test-ui-app');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-app',
generateStories: true,
});
expect(
appTree.read(
'test-ui-app/src/components/my-component/my-component.stories.ts',
'utf-8'
)
).toMatchSnapshot();
});
it('should generate stories for components without interaction tests', async () => {
appTree = await createTestAppLib('test-ui-app');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-app',
generateStories: true,
interactionTests: false,
});
expect(
appTree.read(
'test-ui-app/src/components/my-component/my-component.stories.ts',
'utf-8'
)
).toMatchSnapshot();
});
});
export async function createTestUILib(libName: string): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
linter: Linter.EsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
unitTestRunner: 'none',
name: libName,
projectNameAndRootFormat: 'as-provided',
});
return appTree;
}
export async function createTestAppLib(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await applicationGenerator(appTree, {
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'none',
name: libName,
js: plainJS,
projectNameAndRootFormat: 'as-provided',
});
await componentGenerator(appTree, {
name: 'my-component',
project: libName,
directory: 'app',
});
return appTree;
}

View File

@ -0,0 +1,52 @@
import { StorybookConfigureSchema } from './schema';
import storiesGenerator from '../stories/stories';
import {
convertNxGenerator,
ensurePackage,
formatFiles,
Tree,
} from '@nx/devkit';
import { nxVersion } from '../../utils/versions';
async function generateStories(host: Tree, schema: StorybookConfigureSchema) {
await storiesGenerator(host, {
project: schema.name,
js: schema.js,
ignorePaths: schema.ignorePaths,
skipFormat: true,
interactionTests: schema.interactionTests ?? true,
});
}
export async function storybookConfigurationGenerator(
host: Tree,
schema: StorybookConfigureSchema
) {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/storybook')
>('@nx/storybook', nxVersion);
const installTask = await configurationGenerator(host, {
name: schema.name,
js: schema.js,
linter: schema.linter,
tsConfiguration: schema.tsConfiguration ?? true, // default is true
interactionTests: schema.interactionTests ?? true, // default is true
configureStaticServe: schema.configureStaticServe,
uiFramework: '@storybook/vue3-vite',
skipFormat: true,
});
if (schema.generateStories) {
await generateStories(host, schema);
}
await formatFiles(host);
return installTask;
}
export default storybookConfigurationGenerator;
export const storybookConfigurationSchematic = convertNxGenerator(
storybookConfigurationGenerator
);

View File

@ -0,0 +1,12 @@
import { Linter } from '@nx/linter';
export interface StorybookConfigureSchema {
name: string;
interactionTests?: boolean;
generateStories?: boolean;
js?: boolean;
tsConfiguration?: boolean;
linter?: Linter;
ignorePaths?: string[];
configureStaticServe?: boolean;
}

View File

@ -0,0 +1,77 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxVueStorybookConfigure",
"title": "Vue Storybook Configure",
"description": "Set up Storybook for a Vue project.",
"type": "object",
"properties": {
"name": {
"type": "string",
"aliases": ["project", "projectName"],
"description": "Project for which to generate Storybook configuration.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "For which project do you want to generate Storybook configuration?",
"x-dropdown": "projects",
"x-priority": "important"
},
"interactionTests": {
"type": "boolean",
"description": "Set up Storybook interaction tests.",
"x-prompt": "Do you want to set up Storybook interaction tests?",
"x-priority": "important",
"alias": ["configureTestRunner"],
"default": true
},
"generateStories": {
"type": "boolean",
"description": "Automatically generate `*.stories.ts` files for components declared in this project?",
"x-prompt": "Automatically generate *.stories.ts files for components declared in this project?",
"default": true,
"x-priority": "important"
},
"configureStaticServe": {
"type": "boolean",
"description": "Specifies whether to configure a static file server target for serving storybook. Helpful for speeding up CI build/test times.",
"x-prompt": "Configure a static file server for the storybook instance?",
"default": true,
"x-priority": "important"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript story files rather than TypeScript story files.",
"default": false
},
"tsConfiguration": {
"type": "boolean",
"description": "Configure your project with TypeScript. Generate main.ts and preview.ts files, instead of main.js and preview.js.",
"default": true
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
},
"ignorePaths": {
"type": "array",
"description": "Paths to ignore when looking for components.",
"items": {
"type": "string",
"description": "Path to ignore."
},
"examples": [
"apps/my-app/src/not-stories/**",
"**/**/src/**/not-stories/**",
"libs/my-lib/**/*.something.ts",
"**/**/src/**/*.other.*",
"libs/my-lib/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts"
]
}
},
"required": ["name"],
"examplesFile": "../../../docs/storybook-configuration-examples.md"
}