feat(react-native): add storybook to react native (#8886)
This commit is contained in:
parent
b1e52b67f2
commit
19efdfc938
42
docs/generated/api-react-native/executors/storybook.md
Normal file
42
docs/generated/api-react-native/executors/storybook.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: '@nrwl/react-native:storybook executor'
|
||||||
|
description: 'Serve React Native Storybook'
|
||||||
|
---
|
||||||
|
|
||||||
|
# @nrwl/react-native:storybook
|
||||||
|
|
||||||
|
Serve React Native Storybook
|
||||||
|
|
||||||
|
Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/configuration/projectjson#targets.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### outputFile (_**required**_)
|
||||||
|
|
||||||
|
Default: `./.storybook/story-loader.js`
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The output file that will be written. It is relative to the project directory.
|
||||||
|
|
||||||
|
### pattern (_**required**_)
|
||||||
|
|
||||||
|
Default: `**/*.stories.@(js|jsx|ts|tsx|md)`
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The pattern of files to look at. It can be a specific file, or any valid glob. Note: if using the CLI, globs with \*_/_... must be escaped with quotes
|
||||||
|
|
||||||
|
### searchDir (_**required**_)
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The directory or directories, relative to the project root, to search for files in.
|
||||||
|
|
||||||
|
### silent
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Silences output.
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: '@nrwl/react-native:component-story generator'
|
||||||
|
description: 'Generate storybook story for a react-native component'
|
||||||
|
---
|
||||||
|
|
||||||
|
# @nrwl/react-native:component-story
|
||||||
|
|
||||||
|
Generate storybook story for a react-native component
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx generate component-story ...
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, Nx will search for `component-story` in the default collection provisioned in `workspace.json`.
|
||||||
|
|
||||||
|
You can specify the collection explicitly as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx g @nrwl/react-native:component-story ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Show what will be generated without writing to disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx g component-story ... --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### componentPath (_**required**_)
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Relative path to the component file from the library root
|
||||||
|
|
||||||
|
### project (_**required**_)
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The project name where to add the components.
|
||||||
36
docs/generated/api-react-native/generators/stories.md
Normal file
36
docs/generated/api-react-native/generators/stories.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: '@nrwl/react-native:stories generator'
|
||||||
|
description: 'Create stories for all components declared in an app or library'
|
||||||
|
---
|
||||||
|
|
||||||
|
# @nrwl/react-native:stories
|
||||||
|
|
||||||
|
Create stories for all components declared in an app or library
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx generate stories ...
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, Nx will search for `stories` in the default collection provisioned in `workspace.json`.
|
||||||
|
|
||||||
|
You can specify the collection explicitly as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx g @nrwl/react-native:stories ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Show what will be generated without writing to disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx g stories ... --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### project (_**required**_)
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Library or application name
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: '@nrwl/react-native:storybook-configuration generator'
|
||||||
|
description: 'Set up storybook for a react-native app or library'
|
||||||
|
---
|
||||||
|
|
||||||
|
# @nrwl/react-native:storybook-configuration
|
||||||
|
|
||||||
|
Set up storybook for a react-native app or library
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx generate storybook-configuration ...
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, Nx will search for `storybook-configuration` in the default collection provisioned in `workspace.json`.
|
||||||
|
|
||||||
|
You can specify the collection explicitly as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx g @nrwl/react-native:storybook-configuration ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Show what will be generated without writing to disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx g storybook-configuration ... --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### name (_**required**_)
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Project name
|
||||||
|
|
||||||
|
### generateStories
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Automatically generate \*.stories.ts files for components declared in this project?
|
||||||
|
|
||||||
|
### js
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Generate JavaScript files rather than TypeScript files.
|
||||||
|
|
||||||
|
### linter
|
||||||
|
|
||||||
|
Default: `eslint`
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Possible values: `eslint`, `tslint`
|
||||||
|
|
||||||
|
The tool to use for running lint checks.
|
||||||
|
|
||||||
|
### standaloneConfig
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Split the project configuration into <projectRoot>/project.json rather than including it inside workspace.json
|
||||||
@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: '@nrwl/react:stories generator'
|
title: '@nrwl/react:stories generator'
|
||||||
description: 'Create stories/specs for all components declared in a library'
|
description: 'Create stories/specs for all components declared in an app or library'
|
||||||
---
|
---
|
||||||
|
|
||||||
# @nrwl/react:stories
|
# @nrwl/react:stories
|
||||||
|
|
||||||
Create stories/specs for all components declared in a library
|
Create stories/specs for all components declared in an app or library
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: '@nrwl/react:storybook-configuration generator'
|
title: '@nrwl/react:storybook-configuration generator'
|
||||||
description: 'Set up storybook for a react library'
|
description: 'Set up storybook for a react app or library'
|
||||||
---
|
---
|
||||||
|
|
||||||
# @nrwl/react:storybook-configuration
|
# @nrwl/react:storybook-configuration
|
||||||
|
|
||||||
Set up storybook for a react library
|
Set up storybook for a react app or library
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@ -1198,6 +1198,21 @@
|
|||||||
"id": "library",
|
"id": "library",
|
||||||
"file": "generated/api-react-native/generators/library"
|
"file": "generated/api-react-native/generators/library"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "component-story generator",
|
||||||
|
"id": "component-story",
|
||||||
|
"file": "generated/api-react-native/generators/component-story"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stories generator",
|
||||||
|
"id": "stories",
|
||||||
|
"file": "generated/api-react-native/generators/stories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "storybook-configuration generator",
|
||||||
|
"id": "storybook-configuration",
|
||||||
|
"file": "generated/api-react-native/generators/storybook-configuration"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "build android executor",
|
"name": "build android executor",
|
||||||
"id": "build-android",
|
"id": "build-android",
|
||||||
@ -1228,6 +1243,11 @@
|
|||||||
"id": "start",
|
"id": "start",
|
||||||
"file": "generated/api-react-native/executors/start"
|
"file": "generated/api-react-native/executors/start"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "storybook executor",
|
||||||
|
"id": "storybook",
|
||||||
|
"file": "generated/api-react-native/executors/storybook"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sync deps executor",
|
"name": "sync deps executor",
|
||||||
"id": "sync-deps",
|
"id": "sync-deps",
|
||||||
|
|||||||
@ -57,37 +57,31 @@ describe('react native', () => {
|
|||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
}, 1000000);
|
}, 1000000);
|
||||||
|
|
||||||
xit('should support create application with js', async () => {
|
it('should create storybook with application', async () => {
|
||||||
const appName = uniq('my-app');
|
const appName = uniq('my-app');
|
||||||
runCLI(`generate @nrwl/react-native:application ${appName} --js`);
|
runCLI(`generate @nrwl/react-native:application ${appName}`);
|
||||||
|
runCLI(
|
||||||
|
`generate @nrwl/react-native:storybook-configuration ${appName} --generateStories --no-interactive`
|
||||||
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
checkFilesExist(
|
checkFilesExist(
|
||||||
`apps/${appName}/src/main.js`,
|
`.storybook/story-loader.js`,
|
||||||
`apps/${appName}/src/app/App.js`,
|
`apps/${appName}/.storybook/storybook.ts`,
|
||||||
`apps/${appName}/src/app/App.spec.js`
|
`apps/${appName}/.storybook/toggle-storybook.tsx`,
|
||||||
|
`apps/${appName}/src/app/App.stories.tsx`
|
||||||
)
|
)
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
|
|
||||||
expectTestsPass(await runCLIAsync(`test ${appName}`));
|
await runCLIAsync(`storybook ${appName}`);
|
||||||
|
const result = readJson(join('apps', appName, 'package.json'));
|
||||||
const appLintResults = await runCLIAsync(`lint ${appName}`);
|
expect(result).toMatchObject({
|
||||||
expect(appLintResults.combinedOutput).toContain('All files pass linting.');
|
dependencies: {
|
||||||
|
'@storybook/addon-ondevice-actions': '*',
|
||||||
const iosBundleResult = await runCLIAsync(`bundle-ios ${appName}`);
|
'@storybook/addon-ondevice-backgrounds': '*',
|
||||||
expect(iosBundleResult.combinedOutput).toContain(
|
'@storybook/addon-ondevice-controls': '*',
|
||||||
'Done writing bundle output'
|
'@storybook/addon-ondevice-notes': '*',
|
||||||
);
|
},
|
||||||
expect(() =>
|
});
|
||||||
checkFilesExist(`dist/apps/${appName}/ios/main.jsbundle`)
|
|
||||||
).not.toThrow();
|
|
||||||
|
|
||||||
const androidBundleResult = await runCLIAsync(`bundle-android ${appName}`);
|
|
||||||
expect(androidBundleResult.combinedOutput).toContain(
|
|
||||||
'Done writing bundle output'
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
checkFilesExist(`dist/apps/${appName}/android/main.jsbundle`)
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sync npm dependencies for autolink', async () => {
|
it('sync npm dependencies for autolink', async () => {
|
||||||
|
|||||||
@ -259,10 +259,10 @@
|
|||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-linux-x64-musl": "^1.2.136",
|
|
||||||
"@swc/core-linux-x64-gnu": "^1.2.136",
|
|
||||||
"@swc/core-linux-arm64-gnu": "^1.2.136",
|
"@swc/core-linux-arm64-gnu": "^1.2.136",
|
||||||
"@swc/core-linux-arm64-musl": "^1.2.136"
|
"@swc/core-linux-arm64-musl": "^1.2.136",
|
||||||
|
"@swc/core-linux-x64-gnu": "^1.2.136",
|
||||||
|
"@swc/core-linux-x64-musl": "^1.2.136"
|
||||||
},
|
},
|
||||||
"author": "Victor Savkin",
|
"author": "Victor Savkin",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -34,6 +34,11 @@
|
|||||||
"implementation": "./src/executors/ensure-symlink/ensure-symlink.impl",
|
"implementation": "./src/executors/ensure-symlink/ensure-symlink.impl",
|
||||||
"schema": "./src/executors/ensure-symlink//schema.json",
|
"schema": "./src/executors/ensure-symlink//schema.json",
|
||||||
"description": "Ensure workspace node_modules is symlink under app's node_modules folder."
|
"description": "Ensure workspace node_modules is symlink under app's node_modules folder."
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"implementation": "./src/executors/storybook/storybook.impl",
|
||||||
|
"schema": "./src/executors/storybook/schema.json",
|
||||||
|
"description": "Serve React Native Storybook"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"builders": {
|
"builders": {
|
||||||
@ -71,6 +76,11 @@
|
|||||||
"implementation": "./src/executors/ensure-symlink/compat",
|
"implementation": "./src/executors/ensure-symlink/compat",
|
||||||
"schema": "./src/executors/ensure-symlink//schema.json",
|
"schema": "./src/executors/ensure-symlink//schema.json",
|
||||||
"description": "Ensure workspace node_modules is symlink under app's node_modules folder."
|
"description": "Ensure workspace node_modules is symlink under app's node_modules folder."
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"implementation": "./src/executors/storybook/compat",
|
||||||
|
"schema": "./src/executors/storybook/schema.json",
|
||||||
|
"description": "Serve React Native Storybook"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,24 @@
|
|||||||
"schema": "./src/generators/component/schema.json",
|
"schema": "./src/generators/component/schema.json",
|
||||||
"description": "Create a React Native component",
|
"description": "Create a React Native component",
|
||||||
"aliases": ["c"]
|
"aliases": ["c"]
|
||||||
|
},
|
||||||
|
"storybook-configuration": {
|
||||||
|
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationSchematic",
|
||||||
|
"schema": "./src/generators/storybook-configuration/schema.json",
|
||||||
|
"description": "Set up storybook for a react-native app or library",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"component-story": {
|
||||||
|
"factory": "./src/generators/component-story/component-story#componentStorySchematic",
|
||||||
|
"schema": "./src/generators/component-story/schema.json",
|
||||||
|
"description": "Generate storybook story for a react-native component",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"stories": {
|
||||||
|
"factory": "./src/generators/stories/stories#storiesSchematic",
|
||||||
|
"schema": "./src/generators/stories/schema.json",
|
||||||
|
"description": "Create stories for all components declared in an app or library",
|
||||||
|
"hidden": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generators": {
|
"generators": {
|
||||||
@ -56,6 +74,24 @@
|
|||||||
"schema": "./src/generators/component/schema.json",
|
"schema": "./src/generators/component/schema.json",
|
||||||
"description": "Create a React Native component",
|
"description": "Create a React Native component",
|
||||||
"aliases": ["c"]
|
"aliases": ["c"]
|
||||||
|
},
|
||||||
|
"storybook-configuration": {
|
||||||
|
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationGenerator",
|
||||||
|
"schema": "./src/generators/storybook-configuration/schema.json",
|
||||||
|
"description": "Set up storybook for a react-native app or library",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"component-story": {
|
||||||
|
"factory": "./src/generators/component-story/component-story#componentStoryGenerator",
|
||||||
|
"schema": "./src/generators/component-story/schema.json",
|
||||||
|
"description": "Generate storybook story for a react-native component",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"stories": {
|
||||||
|
"factory": "./src/generators/stories/stories#storiesGenerator",
|
||||||
|
"schema": "./src/generators/stories/schema.json",
|
||||||
|
"description": "Create stories/specs for all components declared in an app or library",
|
||||||
|
"hidden": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"@nrwl/devkit": "*",
|
"@nrwl/devkit": "*",
|
||||||
"@nrwl/jest": "*",
|
"@nrwl/jest": "*",
|
||||||
"@nrwl/linter": "*",
|
"@nrwl/linter": "*",
|
||||||
|
"@nrwl/storybook": "*",
|
||||||
"@nrwl/react": "*",
|
"@nrwl/react": "*",
|
||||||
"@nrwl/workspace": "*",
|
"@nrwl/workspace": "*",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
|
|||||||
5
packages/react-native/src/executors/storybook/compat.ts
Normal file
5
packages/react-native/src/executors/storybook/compat.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { convertNxExecutor } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
import storybookExecutor from './storybook.impl';
|
||||||
|
|
||||||
|
export default convertNxExecutor(storybookExecutor);
|
||||||
7
packages/react-native/src/executors/storybook/schema.d.ts
vendored
Normal file
7
packages/react-native/src/executors/storybook/schema.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// options from https://github.com/elderfo/react-native-storybook-loader#options
|
||||||
|
export interface ReactNativeStorybookOptions {
|
||||||
|
searchDir: string;
|
||||||
|
outputFile: string;
|
||||||
|
pattern: string;
|
||||||
|
silent: boolean;
|
||||||
|
}
|
||||||
28
packages/react-native/src/executors/storybook/schema.json
Normal file
28
packages/react-native/src/executors/storybook/schema.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"title": "React Native Storybook Load Stories",
|
||||||
|
"cli": "nx",
|
||||||
|
"description": "Load stories for react native",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"searchDir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The directory or directories, relative to the project root, to search for files in."
|
||||||
|
},
|
||||||
|
"outputFile": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The output file that will be written. It is relative to the project directory.",
|
||||||
|
"default": "./.storybook/story-loader.js"
|
||||||
|
},
|
||||||
|
"pattern": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The pattern of files to look at. It can be a specific file, or any valid glob. Note: if using the CLI, globs with **/*... must be escaped with quotes",
|
||||||
|
"default": "**/*.stories.@(js|jsx|ts|tsx|md)"
|
||||||
|
},
|
||||||
|
"silent": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Silences output.",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["searchDir", "outputFile", "pattern"]
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { ExecutorContext, logger } from '@nrwl/devkit';
|
||||||
|
import * as chalk from 'chalk';
|
||||||
|
|
||||||
|
import { ReactNativeStorybookOptions } from './schema';
|
||||||
|
import { ChildProcess, fork } from 'child_process';
|
||||||
|
import {
|
||||||
|
displayNewlyAddedDepsMessage,
|
||||||
|
syncDeps,
|
||||||
|
} from '../sync-deps/sync-deps.impl';
|
||||||
|
|
||||||
|
let childProcess: ChildProcess;
|
||||||
|
|
||||||
|
export default async function* reactNatievStorybookExecutor(
|
||||||
|
options: ReactNativeStorybookOptions,
|
||||||
|
context: ExecutorContext
|
||||||
|
): AsyncGenerator<{ success: boolean }> {
|
||||||
|
const projectRoot = context.workspace.projects[context.projectName].root;
|
||||||
|
logger.info(
|
||||||
|
`${chalk.bold.cyan(
|
||||||
|
'info'
|
||||||
|
)} To see your Storybook stories on the device, you should start your mobile app for the <platform> of your choice (typically ios or android).`
|
||||||
|
);
|
||||||
|
|
||||||
|
// add storybook addons to app's package.json
|
||||||
|
displayNewlyAddedDepsMessage(
|
||||||
|
context.projectName,
|
||||||
|
await syncDeps(
|
||||||
|
context.projectName,
|
||||||
|
projectRoot,
|
||||||
|
context.root,
|
||||||
|
'@storybook/addon-ondevice-actions,@storybook/addon-ondevice-backgrounds,@storybook/addon-ondevice-controls,@storybook/addon-ondevice-notes'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runCliStorybook(context.root, projectRoot, options);
|
||||||
|
yield { success: true };
|
||||||
|
} finally {
|
||||||
|
if (childProcess) {
|
||||||
|
childProcess.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCliStorybook(
|
||||||
|
workspaceRoot: string,
|
||||||
|
projectRoot: string,
|
||||||
|
options: ReactNativeStorybookOptions
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
childProcess = fork(
|
||||||
|
join(
|
||||||
|
workspaceRoot,
|
||||||
|
'./node_modules/react-native-storybook-loader/out/rnstl-cli.js'
|
||||||
|
),
|
||||||
|
createStorybookOptions(options),
|
||||||
|
{
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure the child process is killed when the parent exits
|
||||||
|
process.on('exit', () => childProcess.kill());
|
||||||
|
process.on('SIGTERM', () => childProcess.kill());
|
||||||
|
|
||||||
|
childProcess.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
childProcess.on('exit', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(code);
|
||||||
|
} else {
|
||||||
|
reject(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorybookOptions(options) {
|
||||||
|
return Object.keys(options).reduce((acc, k) => {
|
||||||
|
const v = options[k];
|
||||||
|
if (typeof v === 'boolean') {
|
||||||
|
if (v === true) {
|
||||||
|
acc.push(`--${k}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc.push(`--${k}`, options[k]);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ module.exports = (async () => {
|
|||||||
resolver: {
|
resolver: {
|
||||||
assetExts: assetExts.filter((ext) => ext !== 'svg'),
|
assetExts: assetExts.filter((ext) => ext !== 'svg'),
|
||||||
sourceExts: [...sourceExts, 'svg'],
|
sourceExts: [...sourceExts, 'svg'],
|
||||||
|
resolverMainFields: ['sbmodern', 'browser', 'main'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*",
|
"react-native": "*",
|
||||||
"react-native-config": "*",
|
"react-native-config": "*",
|
||||||
"react-native-svg": "*"
|
"react-native-svg": "*",
|
||||||
|
"@react-native-async-storage/async-storage": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import GitHub from './icons/github.svg';
|
|||||||
import Terminal from './icons/terminal.svg';
|
import Terminal from './icons/terminal.svg';
|
||||||
import Heart from './icons/heart.svg';
|
import Heart from './icons/heart.svg';
|
||||||
|
|
||||||
const App = () => {
|
export const App = () => {
|
||||||
const [whatsNextYCoord, setWhatsNextYCoord] = useState<number>(0);
|
const [whatsNextYCoord, setWhatsNextYCoord] = useState<number>(0);
|
||||||
const scrollViewRef = useRef<null | ScrollView>(null);
|
const scrollViewRef = useRef<null | ScrollView>(null);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,445 @@
|
|||||||
|
import { getProjects, Tree, updateProjectConfiguration } from '@nrwl/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
import componentStoryGenerator from './component-story';
|
||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
|
||||||
|
import { formatFile } from '../../utils/format-file';
|
||||||
|
import libraryGenerator from '../library/library';
|
||||||
|
import componentGenerator from '../component/component';
|
||||||
|
|
||||||
|
describe('react-native:component-story', () => {
|
||||||
|
let appTree: Tree;
|
||||||
|
let cmpPath = 'libs/test-ui-lib/src/lib/test-ui-lib/test-ui-lib.tsx';
|
||||||
|
let storyFilePath =
|
||||||
|
'libs/test-ui-lib/src/lib/test-ui-lib/test-ui-lib.stories.tsx';
|
||||||
|
|
||||||
|
describe('default setup', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree = await createTestUILib('test-ui-lib', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when file does not contain a component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`export const add = (a: number, b: number) => a + b;`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail with a descriptive error message', async () => {
|
||||||
|
try {
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toContain(
|
||||||
|
'Could not find any React Native component in file libs/test-ui-lib/src/lib/test-ui-lib/test-ui-lib.tsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('default component setup', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the story file', () => {
|
||||||
|
expect(appTree.exists(storyFilePath)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set up the story', () => {
|
||||||
|
expect(
|
||||||
|
formatFile`${appTree.read(storyFilePath, 'utf-8').replace('·', '')}`
|
||||||
|
).toContain(formatFile`
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
|
||||||
|
const props: TestUiLibProps = {};
|
||||||
|
storiesOf('TestUiLib', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <TestUiLib {...props} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using plain JS components', () => {
|
||||||
|
let storyFilePathPlain =
|
||||||
|
'libs/test-ui-lib/src/lib/test-ui-libplain.stories.jsx';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree.write(
|
||||||
|
'libs/test-ui-lib/src/lib/test-ui-libplain.jsx',
|
||||||
|
`import React from 'react';
|
||||||
|
|
||||||
|
import './test.scss';
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Welcome to test component</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-libplain.jsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the story file', () => {
|
||||||
|
expect(appTree.exists(storyFilePathPlain)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set up the story', () => {
|
||||||
|
expect(formatFile`${appTree.read(storyFilePathPlain, 'utf-8')}`)
|
||||||
|
.toContain(formatFile`
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { Test } from './test-ui-libplain';
|
||||||
|
|
||||||
|
const props = {};
|
||||||
|
|
||||||
|
storiesOf('Test', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <Test {...props} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component without any props defined', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`import React from 'react';
|
||||||
|
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
export function Test() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Welcome to test!</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a story without controls', () => {
|
||||||
|
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
|
||||||
|
.toContain(formatFile`
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { Test } from './test-ui-lib';
|
||||||
|
const props = {};
|
||||||
|
storiesOf('Test', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <Test {...props} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component with props', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup controls based on the component props', () => {
|
||||||
|
expect(
|
||||||
|
formatFile`${appTree.read(storyFilePath, 'utf-8').replace('·', '')}`
|
||||||
|
).toContain(formatFile`
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
|
||||||
|
const props: TestUiLibProps = {};
|
||||||
|
storiesOf('TestUiLib', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <TestUiLib {...props} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component with props and actions', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`import React from 'react';
|
||||||
|
|
||||||
|
export type ButtonStyle = 'default' | 'primary' | 'warning';
|
||||||
|
|
||||||
|
export interface TestProps {
|
||||||
|
name: string;
|
||||||
|
displayAge: boolean;
|
||||||
|
someAction: (e: unknown) => void;
|
||||||
|
style: ButtonStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = (props: TestProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Welcome to test component, {props.name}</h1>
|
||||||
|
<button onClick={props.someAction}>Click me!</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup controls based on the component props', () => {
|
||||||
|
expect(
|
||||||
|
formatFile`${appTree.read(storyFilePath, 'utf-8').replace('·', '')}`
|
||||||
|
).toContain(formatFile`
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { Test, TestProps } from './test-ui-lib';
|
||||||
|
const actions = { someAction: action('someAction executed!') };
|
||||||
|
const props: TestProps = { name: '', displayAge: false };
|
||||||
|
storiesOf('Test', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <Test {...props} {...actions} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'default export function',
|
||||||
|
src: `export default function Test(props: TestProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Welcome to test component, {props.name}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'function and then export',
|
||||||
|
src: `
|
||||||
|
function Test(props: TestProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Welcome to test component, {props.name}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Test;
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arrow function',
|
||||||
|
src: `
|
||||||
|
const Test = (props: TestProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Welcome to test component, {props.name}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Test;
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arrow function without {..}',
|
||||||
|
src: `
|
||||||
|
const Test = (props: TestProps) => <div><h1>Welcome to test component, {props.name}</h1></div>;
|
||||||
|
export default Test
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'direct export of component class',
|
||||||
|
src: `
|
||||||
|
export default class Test extends React.Component<TestProps> {
|
||||||
|
render() {
|
||||||
|
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component class & then default export',
|
||||||
|
src: `
|
||||||
|
class Test extends React.Component<TestProps> {
|
||||||
|
render() {
|
||||||
|
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Test
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PureComponent class & then default export',
|
||||||
|
src: `
|
||||||
|
class Test extends React.PureComponent<TestProps> {
|
||||||
|
render() {
|
||||||
|
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Test
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'direct export of component class new JSX transform',
|
||||||
|
src: `
|
||||||
|
export default class Test extends Component<TestProps> {
|
||||||
|
render() {
|
||||||
|
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component class & then default export new JSX transform',
|
||||||
|
src: `
|
||||||
|
class Test extends Component<TestProps> {
|
||||||
|
render() {
|
||||||
|
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Test
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PureComponent class & then default export new JSX transform',
|
||||||
|
src: `
|
||||||
|
class Test extends PureComponent<TestProps> {
|
||||||
|
render() {
|
||||||
|
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Test
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
].forEach((config) => {
|
||||||
|
describe(`React Native component defined as:${config.name}`, () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`import React from 'react';
|
||||||
|
|
||||||
|
export interface TestProps {
|
||||||
|
name: string;
|
||||||
|
displayAge: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
${config.src}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly setup the controls based on the component props', () => {
|
||||||
|
expect(
|
||||||
|
formatFile`${appTree.read(storyFilePath, 'utf-8').replace('·', '')}`
|
||||||
|
).toContain(formatFile`
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { Test, TestProps } from './test-ui-lib';
|
||||||
|
const props: TestProps = {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
};
|
||||||
|
storiesOf('Test', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <Test {...props} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('using eslint', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree = await createTestUILib('test-ui-lib', false);
|
||||||
|
await componentGenerator(appTree, {
|
||||||
|
name: 'test-ui-lib',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
export: true,
|
||||||
|
});
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set up the story', () => {
|
||||||
|
expect(formatFile`${appTree.read(storyFilePath, 'utf-8')}`)
|
||||||
|
.toContain(formatFile`
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
|
||||||
|
|
||||||
|
const props: TestUiLibProps = {};
|
||||||
|
|
||||||
|
storiesOf('TestUiLib', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => <TestUiLib {...props} />);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createTestUILib(
|
||||||
|
libName: string,
|
||||||
|
useEsLint = false
|
||||||
|
): Promise<Tree> {
|
||||||
|
let appTree = createTreeWithEmptyWorkspace();
|
||||||
|
appTree.write('.gitignore', '');
|
||||||
|
|
||||||
|
await libraryGenerator(appTree, {
|
||||||
|
name: libName,
|
||||||
|
linter: useEsLint ? Linter.EsLint : Linter.TsLint,
|
||||||
|
skipFormat: true,
|
||||||
|
skipTsConfig: false,
|
||||||
|
unitTestRunner: 'jest',
|
||||||
|
});
|
||||||
|
|
||||||
|
await componentGenerator(appTree, {
|
||||||
|
name: libName,
|
||||||
|
project: libName,
|
||||||
|
export: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (useEsLint) {
|
||||||
|
const currentWorkspaceJson = getProjects(appTree);
|
||||||
|
|
||||||
|
const projectConfig = currentWorkspaceJson.get(libName);
|
||||||
|
projectConfig.targets.lint.options.linter = 'eslint';
|
||||||
|
|
||||||
|
updateProjectConfiguration(appTree, libName, projectConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return appTree;
|
||||||
|
}
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
convertNxGenerator,
|
||||||
|
formatFiles,
|
||||||
|
generateFiles,
|
||||||
|
getProjects,
|
||||||
|
joinPathFragments,
|
||||||
|
normalizePath,
|
||||||
|
Tree,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import {
|
||||||
|
getComponentName,
|
||||||
|
getComponentPropsInterface,
|
||||||
|
} from '@nrwl/react/src/utils/ast-utils';
|
||||||
|
import { CreateComponentStoriesFileSchema } from './schema';
|
||||||
|
|
||||||
|
export function getArgsDefaultValue(property: ts.SyntaxKind): string {
|
||||||
|
const typeNameToDefault: Record<number, any> = {
|
||||||
|
[ts.SyntaxKind.StringKeyword]: "''",
|
||||||
|
[ts.SyntaxKind.NumberKeyword]: 0,
|
||||||
|
[ts.SyntaxKind.BooleanKeyword]: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvedValue = typeNameToDefault[property];
|
||||||
|
if (typeof resolvedValue === undefined) {
|
||||||
|
return "''";
|
||||||
|
} else {
|
||||||
|
return resolvedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createComponentStoriesFile(
|
||||||
|
host: Tree,
|
||||||
|
{ project, componentPath }: CreateComponentStoriesFileSchema
|
||||||
|
) {
|
||||||
|
const proj = getProjects(host).get(project);
|
||||||
|
const sourceRoot = proj.sourceRoot;
|
||||||
|
|
||||||
|
const componentFilePath = joinPathFragments(sourceRoot, componentPath);
|
||||||
|
const componentDirectory = componentFilePath.replace(
|
||||||
|
componentFilePath.slice(componentFilePath.lastIndexOf('/')),
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPlainJs =
|
||||||
|
componentFilePath.endsWith('.jsx') || componentFilePath.endsWith('.js');
|
||||||
|
let fileExt = 'tsx';
|
||||||
|
if (componentFilePath.endsWith('.jsx')) {
|
||||||
|
fileExt = 'jsx';
|
||||||
|
} else if (componentFilePath.endsWith('.js')) {
|
||||||
|
fileExt = 'js';
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentFileName = componentFilePath
|
||||||
|
.slice(componentFilePath.lastIndexOf('/') + 1)
|
||||||
|
.replace('.tsx', '')
|
||||||
|
.replace('.jsx', '')
|
||||||
|
.replace('.js', '');
|
||||||
|
|
||||||
|
const name = componentFileName;
|
||||||
|
|
||||||
|
const contents = host.read(componentFilePath, 'utf-8');
|
||||||
|
if (contents === null) {
|
||||||
|
throw new Error(`Failed to read ${componentFilePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFile = ts.createSourceFile(
|
||||||
|
componentFilePath,
|
||||||
|
contents,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const cmpDeclaration = getComponentName(sourceFile);
|
||||||
|
|
||||||
|
if (!cmpDeclaration) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find any React Native component in file ${componentFilePath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsInterface = getComponentPropsInterface(sourceFile);
|
||||||
|
|
||||||
|
let propsTypeName: string = null;
|
||||||
|
let props: {
|
||||||
|
name: string;
|
||||||
|
defaultValue: any;
|
||||||
|
}[] = [];
|
||||||
|
let argTypes: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
actionText: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
if (propsInterface) {
|
||||||
|
propsTypeName = propsInterface.name.text;
|
||||||
|
props = propsInterface.members.map((member: ts.PropertySignature) => {
|
||||||
|
if (member.type.kind === ts.SyntaxKind.FunctionType) {
|
||||||
|
argTypes.push({
|
||||||
|
name: (member.name as ts.Identifier).text,
|
||||||
|
type: 'action',
|
||||||
|
actionText: `${(member.name as ts.Identifier).text} executed!`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: (member.name as ts.Identifier).text,
|
||||||
|
defaultValue: getArgsDefaultValue(member.type.kind),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
props = props.filter((p) => p && p.defaultValue !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFiles(
|
||||||
|
host,
|
||||||
|
joinPathFragments(__dirname, './files'),
|
||||||
|
normalizePath(componentDirectory),
|
||||||
|
{
|
||||||
|
componentFileName: name,
|
||||||
|
propsTypeName,
|
||||||
|
props,
|
||||||
|
argTypes,
|
||||||
|
componentName: (cmpDeclaration as any).name.text,
|
||||||
|
isPlainJs,
|
||||||
|
fileExt,
|
||||||
|
hasActions: argTypes && argTypes.length,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function componentStoryGenerator(
|
||||||
|
host: Tree,
|
||||||
|
schema: CreateComponentStoriesFileSchema
|
||||||
|
) {
|
||||||
|
createComponentStoriesFile(host, schema);
|
||||||
|
await formatFiles(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default componentStoryGenerator;
|
||||||
|
export const componentStorySchematic = convertNxGenerator(
|
||||||
|
componentStoryGenerator
|
||||||
|
);
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<% if (hasActions) { %>
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
<% } %>
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
<%= componentName %>
|
||||||
|
<% if ( propsTypeName ) { %>, <%= propsTypeName %> <% } %>
|
||||||
|
} from './<%= componentFileName %>';
|
||||||
|
|
||||||
|
<% if (hasActions) { %>
|
||||||
|
const actions = {<% for (let argType of argTypes) { %>
|
||||||
|
<%= argType.name %>: action('<%- argType.actionText %>'),
|
||||||
|
<% } %>};
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
const props <% if ( propsTypeName ) { %>:<%= propsTypeName %><% } %> = {<% for (let prop of props) { %>
|
||||||
|
<%= prop.name %>: <%- prop.defaultValue %>,
|
||||||
|
<% } %>};
|
||||||
|
|
||||||
|
storiesOf('<%= componentName %>', module)
|
||||||
|
.addDecorator((getStory) => <>{getStory()}</>)
|
||||||
|
.add('Primary', () => (
|
||||||
|
<<%= componentName %> {...props} <% if (hasActions) { %> {...actions} <% } %>/>
|
||||||
|
));
|
||||||
4
packages/react-native/src/generators/component-story/schema.d.ts
vendored
Normal file
4
packages/react-native/src/generators/component-story/schema.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface CreateComponentStoriesFileSchema {
|
||||||
|
project: string;
|
||||||
|
componentPath: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"cli": "nx",
|
||||||
|
"$id": "NxReactNativeComponentStory",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The project name where to add the components.",
|
||||||
|
"examples": ["shared-ui-component"],
|
||||||
|
"$default": {
|
||||||
|
"$source": "projectName",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"x-prompt": "What's the name of the project where the component lives?"
|
||||||
|
},
|
||||||
|
"componentPath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Relative path to the component file from the library root",
|
||||||
|
"examples": ["lib/components"],
|
||||||
|
"x-prompt": "What's path of the component relative to the project's lib root?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["project", "componentPath"]
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ import {
|
|||||||
metroReactNativeBabelPresetVersion,
|
metroReactNativeBabelPresetVersion,
|
||||||
metroVersion,
|
metroVersion,
|
||||||
nxVersion,
|
nxVersion,
|
||||||
|
reactNativeAsyncStorageVersion,
|
||||||
reactNativeCommunityCli,
|
reactNativeCommunityCli,
|
||||||
reactNativeCommunityCliAndroid,
|
reactNativeCommunityCliAndroid,
|
||||||
reactNativeCommunityCliIos,
|
reactNativeCommunityCliIos,
|
||||||
@ -88,6 +89,8 @@ export function updateDependencies(host: Tree) {
|
|||||||
'react-native-svg-transformer': reactNativeSvgTransformerVersion,
|
'react-native-svg-transformer': reactNativeSvgTransformerVersion,
|
||||||
'react-native-svg': reactNativeSvgVersion,
|
'react-native-svg': reactNativeSvgVersion,
|
||||||
'react-native-config': reactNativeConfigVersion,
|
'react-native-config': reactNativeConfigVersion,
|
||||||
|
'@react-native-async-storage/async-storage':
|
||||||
|
reactNativeAsyncStorageVersion,
|
||||||
...(isPnpm
|
...(isPnpm
|
||||||
? {
|
? {
|
||||||
'metro-config': metroVersion, // metro-config is used by metro.config.js
|
'metro-config': metroVersion, // metro-config is used by metro.config.js
|
||||||
|
|||||||
3
packages/react-native/src/generators/stories/schema.d.ts
vendored
Normal file
3
packages/react-native/src/generators/stories/schema.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface StorybookStoriesSchema {
|
||||||
|
project: string;
|
||||||
|
}
|
||||||
18
packages/react-native/src/generators/stories/schema.json
Normal file
18
packages/react-native/src/generators/stories/schema.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"cli": "nx",
|
||||||
|
"$id": "NxReactNativeStorybookStories",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Library or application name",
|
||||||
|
"$default": {
|
||||||
|
"$source": "projectName",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"x-prompt": "What's the name of the project for which you want to generate stories?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["project"]
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { Tree } from '@nrwl/devkit';
|
||||||
|
import storiesGenerator from './stories';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
import applicationGenerator from '../application/application';
|
||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
import { reactNativeComponentGenerator } from '../component/component';
|
||||||
|
|
||||||
|
describe('react:stories for applications', () => {
|
||||||
|
let appTree: Tree;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree = await createTestUIApp('test-ui-app');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the stories', async () => {
|
||||||
|
await reactNativeComponentGenerator(appTree, {
|
||||||
|
name: 'another-cmp',
|
||||||
|
project: 'test-ui-app',
|
||||||
|
});
|
||||||
|
await storiesGenerator(appTree, {
|
||||||
|
project: 'test-ui-app',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(appTree.exists('apps/test-ui-app/src/app/App.tsx')).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
appTree.exists('apps/test-ui-app/src/app/App.stories.tsx')
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
appTree.exists(
|
||||||
|
'apps/test-ui-app/src/app/another-cmp/another-cmp.stories.tsx'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore files that do not contain components', async () => {
|
||||||
|
// create another component
|
||||||
|
appTree.write(
|
||||||
|
'apps/test-ui-app/src/app/some-utils.js',
|
||||||
|
`export const add = (a: number, b: number) => a + b;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await storiesGenerator(appTree, {
|
||||||
|
project: 'test-ui-app',
|
||||||
|
});
|
||||||
|
|
||||||
|
// should just create the story and not error, even though there's a js file
|
||||||
|
// not containing any react component
|
||||||
|
expect(
|
||||||
|
appTree.exists('apps/test-ui-app/src/app/App.stories.tsx')
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createTestUIApp(libName: string): Promise<Tree> {
|
||||||
|
let appTree = createTreeWithEmptyWorkspace();
|
||||||
|
appTree.write('.gitignore', '');
|
||||||
|
|
||||||
|
await applicationGenerator(appTree, {
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: false,
|
||||||
|
style: 'css',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: libName,
|
||||||
|
});
|
||||||
|
return appTree;
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { Tree } from '@nrwl/devkit';
|
||||||
|
import storiesGenerator from './stories';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
import applicationGenerator from '../application/application';
|
||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
import libraryGenerator from '../library/library';
|
||||||
|
import reactNativeComponentGenerator from '../component/component';
|
||||||
|
|
||||||
|
describe('react-native:stories for libraries', () => {
|
||||||
|
let appTree: Tree;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
appTree = await createTestUILib('test-ui-lib');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the stories', async () => {
|
||||||
|
await reactNativeComponentGenerator(appTree, {
|
||||||
|
name: 'test-ui-lib',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
await reactNativeComponentGenerator(appTree, {
|
||||||
|
name: 'another-cmp',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
await storiesGenerator(appTree, {
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appTree.exists(
|
||||||
|
'libs/test-ui-lib/src/lib/test-ui-lib/test-ui-lib.stories.tsx'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
appTree.exists(
|
||||||
|
'libs/test-ui-lib/src/lib/another-cmp/another-cmp.stories.tsx'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore files that do not contain components', async () => {
|
||||||
|
await reactNativeComponentGenerator(appTree, {
|
||||||
|
name: 'test-ui-lib',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
// create another component
|
||||||
|
appTree.write(
|
||||||
|
'libs/test-ui-lib/src/lib/some-utils.ts',
|
||||||
|
`export const add = (a: number, b: number) => a + b;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await storiesGenerator(appTree, {
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
|
||||||
|
// should just create the story and not error, even though there's a js file
|
||||||
|
// not containing any react component
|
||||||
|
expect(
|
||||||
|
appTree.exists(
|
||||||
|
'libs/test-ui-lib/src/lib/test-ui-lib/test-ui-lib.stories.tsx'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createTestUILib(libName: string): Promise<Tree> {
|
||||||
|
let appTree = createTreeWithEmptyWorkspace();
|
||||||
|
appTree.write('.gitignore', '');
|
||||||
|
|
||||||
|
await libraryGenerator(appTree, {
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: true,
|
||||||
|
skipTsConfig: false,
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: libName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await applicationGenerator(appTree, {
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: false,
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: `${libName}-e2e`,
|
||||||
|
});
|
||||||
|
return appTree;
|
||||||
|
}
|
||||||
94
packages/react-native/src/generators/stories/stories.ts
Normal file
94
packages/react-native/src/generators/stories/stories.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { getComponentName } from '@nrwl/react/src/utils/ast-utils';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import {
|
||||||
|
convertNxGenerator,
|
||||||
|
getProjects,
|
||||||
|
joinPathFragments,
|
||||||
|
ProjectType,
|
||||||
|
Tree,
|
||||||
|
visitNotIgnoredFiles,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import componentStoryGenerator from '../component-story/component-story';
|
||||||
|
import { StorybookStoriesSchema } from './schema';
|
||||||
|
|
||||||
|
export function projectRootPath(
|
||||||
|
tree: Tree,
|
||||||
|
sourceRoot: string,
|
||||||
|
projectType: ProjectType
|
||||||
|
): string {
|
||||||
|
let projectDir = '';
|
||||||
|
if (projectType === 'application') {
|
||||||
|
// apps/test-app/src/app
|
||||||
|
projectDir = 'app';
|
||||||
|
} else if (projectType == 'library') {
|
||||||
|
// libs/test-lib/src/lib
|
||||||
|
projectDir = 'lib';
|
||||||
|
}
|
||||||
|
|
||||||
|
return joinPathFragments(sourceRoot, projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsComponentDeclaration(
|
||||||
|
tree: Tree,
|
||||||
|
componentPath: string
|
||||||
|
): boolean {
|
||||||
|
const contents = tree.read(componentPath, 'utf-8');
|
||||||
|
if (contents === null) {
|
||||||
|
throw new Error(`Failed to read ${componentPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFile = ts.createSourceFile(
|
||||||
|
componentPath,
|
||||||
|
contents,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!getComponentName(sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAllStories(tree: Tree, projectName: string) {
|
||||||
|
const projects = getProjects(tree);
|
||||||
|
const project = projects.get(projectName);
|
||||||
|
|
||||||
|
const { sourceRoot, projectType } = project;
|
||||||
|
const projectPath = projectRootPath(tree, sourceRoot, projectType);
|
||||||
|
|
||||||
|
let componentPaths: string[] = [];
|
||||||
|
visitNotIgnoredFiles(tree, projectPath, (path) => {
|
||||||
|
if (
|
||||||
|
(path.endsWith('.tsx') && !path.endsWith('.spec.tsx')) ||
|
||||||
|
(path.endsWith('.js') && !path.endsWith('.spec.js')) ||
|
||||||
|
(path.endsWith('.jsx') && !path.endsWith('.spec.jsx'))
|
||||||
|
) {
|
||||||
|
componentPaths.push(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
componentPaths.map(async (componentPath) => {
|
||||||
|
const relativeCmpDir = componentPath.replace(join(sourceRoot, '/'), '');
|
||||||
|
|
||||||
|
if (!containsComponentDeclaration(tree, componentPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await componentStoryGenerator(tree, {
|
||||||
|
componentPath: relativeCmpDir,
|
||||||
|
project: projectName,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storiesGenerator(
|
||||||
|
host: Tree,
|
||||||
|
schema: StorybookStoriesSchema
|
||||||
|
) {
|
||||||
|
await createAllStories(host, schema.project);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default storiesGenerator;
|
||||||
|
export const storiesSchematic = convertNxGenerator(storiesGenerator);
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
import * as fileUtils from '@nrwl/workspace/src/core/file-utils';
|
||||||
|
import { Tree } from '@nrwl/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
import { logger } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
import libraryGenerator from '../library/library';
|
||||||
|
import applicationGenerator from '../application/application';
|
||||||
|
import componentGenerator from '../component/component';
|
||||||
|
import storybookConfigurationGenerator from './configuration';
|
||||||
|
|
||||||
|
describe('react-native:storybook-configuration', () => {
|
||||||
|
let appTree;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.spyOn(fileUtils, 'readPackageJson').mockReturnValue({
|
||||||
|
devDependencies: {
|
||||||
|
'@storybook/addon-essentials': '*',
|
||||||
|
'@storybook/react-native': '*',
|
||||||
|
'@storybook/addon-ondevice-actions': '*',
|
||||||
|
'@storybook/addon-ondevice-knobs': '*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(logger, 'warn').mockImplementation(() => {});
|
||||||
|
jest.spyOn(logger, 'debug').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should generate files for an app', () => {
|
||||||
|
it('should configure everything at once', async () => {
|
||||||
|
appTree = await createTestUILib('test-ui-lib');
|
||||||
|
appTree.write('.gitignore', '');
|
||||||
|
await storybookConfigurationGenerator(appTree, {
|
||||||
|
name: 'test-ui-lib',
|
||||||
|
standaloneConfig: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appTree.exists('libs/test-ui-lib/.storybook/main.js')
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
appTree.exists('libs/test-ui-lib/.storybook/tsconfig.json')
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate stories for components', async () => {
|
||||||
|
appTree = await createTestUILib('test-ui-lib');
|
||||||
|
await componentGenerator(appTree, {
|
||||||
|
name: 'test-ui-lib',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
await storybookConfigurationGenerator(appTree, {
|
||||||
|
name: 'test-ui-lib',
|
||||||
|
generateStories: true,
|
||||||
|
standaloneConfig: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appTree.exists(
|
||||||
|
'libs/test-ui-lib/src/lib/test-ui-lib/test-ui-lib.stories.tsx'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should generate files for lib', () => {
|
||||||
|
it('should configure everything at once', async () => {
|
||||||
|
appTree = await createTestAppLib('test-ui-app');
|
||||||
|
await storybookConfigurationGenerator(appTree, {
|
||||||
|
name: 'test-ui-app',
|
||||||
|
standaloneConfig: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appTree.exists('apps/test-ui-app/.storybook/main.js')
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
appTree.exists('apps/test-ui-app/.storybook/tsconfig.json')
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate stories for components', async () => {
|
||||||
|
appTree = await createTestAppLib('test-ui-app');
|
||||||
|
await storybookConfigurationGenerator(appTree, {
|
||||||
|
name: 'test-ui-app',
|
||||||
|
generateStories: true,
|
||||||
|
standaloneConfig: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Currently the auto-generate stories feature only picks up components under the 'lib' directory.
|
||||||
|
// In our 'createTestAppLib' function, we call @nrwl/react-native:component to generate a component
|
||||||
|
// under the specified 'lib' directory
|
||||||
|
expect(
|
||||||
|
appTree.exists(
|
||||||
|
'apps/test-ui-app/src/app/my-component/my-component.stories.tsx'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createTestUILib(libName: string): Promise<Tree> {
|
||||||
|
let appTree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
await libraryGenerator(appTree, {
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: true,
|
||||||
|
skipTsConfig: false,
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
name: libName,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
await componentGenerator(appTree, {
|
||||||
|
name: 'my-component',
|
||||||
|
project: libName,
|
||||||
|
directory: 'app',
|
||||||
|
});
|
||||||
|
|
||||||
|
return appTree;
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
convertNxGenerator,
|
||||||
|
GeneratorCallback,
|
||||||
|
readProjectConfiguration,
|
||||||
|
Tree,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { configurationGenerator } from '@nrwl/storybook';
|
||||||
|
|
||||||
|
import storiesGenerator from '../stories/stories';
|
||||||
|
import { createStorybookFiles } from './lib/create-storybook-files';
|
||||||
|
import { replaceAppImportWithStorybookToggle } from './lib/replace-app-import-with-storybook-toggle';
|
||||||
|
|
||||||
|
import { StorybookConfigureSchema } from './schema';
|
||||||
|
|
||||||
|
async function generateStories(host: Tree, schema: StorybookConfigureSchema) {
|
||||||
|
await storiesGenerator(host, {
|
||||||
|
project: schema.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storybookConfigurationGenerator(
|
||||||
|
host: Tree,
|
||||||
|
schema: StorybookConfigureSchema
|
||||||
|
): Promise<GeneratorCallback> {
|
||||||
|
const installTask = await configurationGenerator(host, {
|
||||||
|
name: schema.name,
|
||||||
|
uiFramework: '@storybook/react-native',
|
||||||
|
configureCypress: false,
|
||||||
|
js: false,
|
||||||
|
linter: schema.linter,
|
||||||
|
standaloneConfig: schema.standaloneConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
addStorybookTask(host, schema.name);
|
||||||
|
createStorybookFiles(host, schema);
|
||||||
|
replaceAppImportWithStorybookToggle(host, schema);
|
||||||
|
|
||||||
|
if (schema.generateStories) {
|
||||||
|
await generateStories(host, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
return installTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStorybookTask(host: Tree, projectName: string) {
|
||||||
|
const projectConfig = readProjectConfiguration(host, projectName);
|
||||||
|
projectConfig.targets['storybook'] = {
|
||||||
|
executor: '@nrwl/react-native:storybook',
|
||||||
|
options: {
|
||||||
|
searchDir: projectConfig.root,
|
||||||
|
outputFile: './.storybook/story-loader.js',
|
||||||
|
pattern: '**/*.stories.@(js|jsx|ts|tsx|md)',
|
||||||
|
silent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
updateProjectConfiguration(host, projectName, projectConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default storybookConfigurationGenerator;
|
||||||
|
export const storybookConfigurationSchematic = convertNxGenerator(
|
||||||
|
storybookConfigurationGenerator
|
||||||
|
);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { configure, getStorybookUI } from '@storybook/react-native';
|
||||||
|
|
||||||
|
import { loadStories } from '../../../.storybook/story-loader';
|
||||||
|
|
||||||
|
configure(loadStories(), module, false);
|
||||||
|
|
||||||
|
const StorybookUIRoot = getStorybookUI({});
|
||||||
|
|
||||||
|
export default StorybookUIRoot;
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Toggle inspired from https://github.com/infinitered/ignite/blob/master/boilerplate/storybook/toggle-storybook.tsx
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { DevSettings } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
import AppRoot from '../src/app/App';
|
||||||
|
|
||||||
|
export const DEFAULT_REACTOTRON_WS_URI = 'ws://localhost:9090';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a string from storage.
|
||||||
|
*
|
||||||
|
* @param key The key to fetch.
|
||||||
|
*/
|
||||||
|
async function loadString(key: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
return await AsyncStorage.getItem(key);
|
||||||
|
} catch {
|
||||||
|
// not sure why this would fail... even reading the RN docs I'm unclear
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a string to storage.
|
||||||
|
*
|
||||||
|
* @param key The key to fetch.
|
||||||
|
* @param value The value to store.
|
||||||
|
*/
|
||||||
|
async function saveString(key: string, value: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(key, value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle Storybook mode, in __DEV__ mode only.
|
||||||
|
*
|
||||||
|
* In non-__DEV__ mode, or when Storybook isn't toggled on,
|
||||||
|
* renders its children.
|
||||||
|
*
|
||||||
|
* The mode flag is persisted in async storage, which means it
|
||||||
|
* persists across reloads/restarts - this is handy when developing
|
||||||
|
* new components in Storybook.
|
||||||
|
*/
|
||||||
|
function ToggleStorybook(props) {
|
||||||
|
const [showStorybook, setShowStorybook] = useState(false);
|
||||||
|
const [StorybookUIRoot, setStorybookUIRoot] = useState(null);
|
||||||
|
const ws = useRef(new WebSocket(DEFAULT_REACTOTRON_WS_URI));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!__DEV__) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the setting from storage if it's there
|
||||||
|
loadString('devStorybook').then((storedSetting) => {
|
||||||
|
// Set the initial value
|
||||||
|
setShowStorybook(storedSetting === 'on');
|
||||||
|
|
||||||
|
if (DevSettings) {
|
||||||
|
// Add our toggle command to the menu
|
||||||
|
DevSettings.addMenuItem('Toggle Storybook', () => {
|
||||||
|
setShowStorybook((show) => {
|
||||||
|
// On toggle, flip the current value
|
||||||
|
show = !show;
|
||||||
|
|
||||||
|
// Write it back to storage
|
||||||
|
saveString('devStorybook', show ? 'on' : 'off');
|
||||||
|
|
||||||
|
// Return it to change the local state
|
||||||
|
return show;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the storybook UI once
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
setStorybookUIRoot(() => require('./storybook.ts').default);
|
||||||
|
|
||||||
|
// Behave as Reactotron.storybookSwitcher(), not a HOC way.
|
||||||
|
ws.current.onmessage = (e) => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (data.type === 'storybook') {
|
||||||
|
saveString('devStorybook', data.payload ? 'on' : 'off');
|
||||||
|
setShowStorybook(data.payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.current.onerror = (e) => {
|
||||||
|
setShowStorybook(storedSetting === 'on');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (showStorybook) {
|
||||||
|
return StorybookUIRoot ? <StorybookUIRoot /> : null;
|
||||||
|
} else {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<ToggleStorybook>
|
||||||
|
<AppRoot />
|
||||||
|
</ToggleStorybook>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
// Auto-generated file created by react-native-storybook-loader
|
||||||
|
// Do not edit.
|
||||||
|
//
|
||||||
|
// https://github.com/elderfo/react-native-storybook-loader.git
|
||||||
|
|
||||||
|
function loadStories() {}
|
||||||
|
|
||||||
|
const stories = [];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadStories,
|
||||||
|
stories,
|
||||||
|
};
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
generateFiles,
|
||||||
|
logger,
|
||||||
|
offsetFromRoot,
|
||||||
|
readProjectConfiguration,
|
||||||
|
toJS,
|
||||||
|
Tree,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { join } from 'path';
|
||||||
|
import * as chalk from 'chalk';
|
||||||
|
|
||||||
|
import { StorybookConfigureSchema } from '../schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function generate ./storybook under project root.
|
||||||
|
*/
|
||||||
|
export async function createStorybookFiles(
|
||||||
|
host: Tree,
|
||||||
|
schema: StorybookConfigureSchema
|
||||||
|
) {
|
||||||
|
const { root, projectType, targets, sourceRoot } = readProjectConfiguration(
|
||||||
|
host,
|
||||||
|
schema.name
|
||||||
|
);
|
||||||
|
|
||||||
|
// do not proceed if not a react native project
|
||||||
|
if (targets?.start?.executor !== '@nrwl/react-native:start') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storybookUIFileName = schema.js ? 'storybook.js' : 'storybook.ts';
|
||||||
|
const storybookUIFilePath = join(root, `./${storybookUIFileName}`);
|
||||||
|
|
||||||
|
if (host.exists(storybookUIFilePath)) {
|
||||||
|
logger.warn(
|
||||||
|
`${storybookUIFileName} file already exists for React Native ${projectType} ${schema.name}! Skipping generating this file.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (projectType !== 'application') {
|
||||||
|
logger.info(
|
||||||
|
`${chalk.bold.cyan(
|
||||||
|
'info'
|
||||||
|
)} To see your Storybook stories on the device, you should start your mobile app for the <platform> of your choice (typically ios or android).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDirectory = projectType === 'application' ? 'app' : 'lib';
|
||||||
|
|
||||||
|
logger.debug(`Adding storybook file to React Native app ${projectDirectory}`);
|
||||||
|
|
||||||
|
// copy files to app's .storybook folder
|
||||||
|
generateFiles(
|
||||||
|
host,
|
||||||
|
join(__dirname, '../files/app'),
|
||||||
|
join(root, '.storybook'),
|
||||||
|
{
|
||||||
|
tmpl: '',
|
||||||
|
offsetFromRoot: offsetFromRoot(sourceRoot),
|
||||||
|
projectType: projectDirectory,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// copy files to workspace root's .storybook folder
|
||||||
|
generateFiles(
|
||||||
|
host,
|
||||||
|
join(__dirname, '../files/root'),
|
||||||
|
join(root, offsetFromRoot(root), '.storybook'),
|
||||||
|
{
|
||||||
|
tmpl: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (schema.js) {
|
||||||
|
toJS(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { addProjectConfiguration, Tree } from '@nrwl/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||||
|
|
||||||
|
import { formatFile } from '../../../utils/format-file';
|
||||||
|
|
||||||
|
import { replaceAppImportWithStorybookToggle } from './replace-app-import-with-storybook-toggle';
|
||||||
|
|
||||||
|
describe('replaceAppImportWithStorybookToggle', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
addProjectConfiguration(tree, 'products', {
|
||||||
|
root: 'apps/products',
|
||||||
|
sourceRoot: 'apps/products/src',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the main file with import from storybook', async () => {
|
||||||
|
tree.write(
|
||||||
|
'/apps/products/src/main.tsx',
|
||||||
|
formatFile`import { AppRegistry } from 'react-native';
|
||||||
|
import App from './app/App';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('main', () => App);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
replaceAppImportWithStorybookToggle(tree, {
|
||||||
|
name: 'products',
|
||||||
|
js: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainFile = tree.read('apps/products/src/main.tsx', 'utf-8');
|
||||||
|
expect(formatFile`${mainFile}`).toEqual(
|
||||||
|
formatFile`import { AppRegistry } from 'react-native';
|
||||||
|
import App from '../.storybook/toggle-storybook';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('main', () => App);`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update the main file if import is already updated', async () => {
|
||||||
|
tree.write(
|
||||||
|
'/apps/products/src/main.tsx',
|
||||||
|
formatFile`import { AppRegistry } from 'react-native';
|
||||||
|
import App from './app/App';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('main', () => App);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
replaceAppImportWithStorybookToggle(tree, {
|
||||||
|
name: 'products',
|
||||||
|
js: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainFile = tree.read('apps/products/src/main.tsx', 'utf-8');
|
||||||
|
expect(formatFile`${mainFile}`).toEqual(
|
||||||
|
formatFile`import { AppRegistry } from 'react-native';
|
||||||
|
import App from '../.storybook/toggle-storybook';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('main', () => App);`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
logger,
|
||||||
|
readProjectConfiguration,
|
||||||
|
stripIndents,
|
||||||
|
Tree,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { StorybookConfigureSchema } from '../schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To replace the import statement for storybook.
|
||||||
|
* Need to import app with storybook toggle from .storybook/toggle-storybook
|
||||||
|
*/
|
||||||
|
export function replaceAppImportWithStorybookToggle(
|
||||||
|
host: Tree,
|
||||||
|
schema: StorybookConfigureSchema
|
||||||
|
) {
|
||||||
|
const { root, sourceRoot } = readProjectConfiguration(host, schema.name);
|
||||||
|
|
||||||
|
const mainFilePath = join(sourceRoot, schema.js ? 'main.js' : 'main.tsx');
|
||||||
|
const appImportImport = `import App from './app/App';`;
|
||||||
|
const storybookeToggleImport = `import App from '../.storybook/toggle-storybook';`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug(`Updating import for ${mainFilePath}`);
|
||||||
|
const contents = host.read(mainFilePath, 'utf-8');
|
||||||
|
if (
|
||||||
|
!contents.includes(appImportImport) ||
|
||||||
|
contents.includes(storybookeToggleImport)
|
||||||
|
) {
|
||||||
|
logger.warn(stripIndents`${mainFilePath} is already udpated.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
host.write(
|
||||||
|
mainFilePath,
|
||||||
|
contents.replace(appImportImport, storybookeToggleImport)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
logger.warn(
|
||||||
|
stripIndents`Unable to update import in ${mainFilePath} for project ${root}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/react-native/src/generators/storybook-configuration/schema.d.ts
vendored
Normal file
9
packages/react-native/src/generators/storybook-configuration/schema.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Linter } from '@nrwl/linter';
|
||||||
|
|
||||||
|
export interface StorybookConfigureSchema {
|
||||||
|
name: string;
|
||||||
|
generateStories?: boolean;
|
||||||
|
js?: boolean;
|
||||||
|
linter?: Linter;
|
||||||
|
standaloneConfig?: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"cli": "nx",
|
||||||
|
"$id": "NxReactNativeStorybookConfigure",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project name",
|
||||||
|
"$default": {
|
||||||
|
"$source": "argv",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"js": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Generate JavaScript files rather than TypeScript files.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"description": "The tool to use for running lint checks.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["eslint", "tslint"],
|
||||||
|
"default": "eslint"
|
||||||
|
},
|
||||||
|
"standaloneConfig": {
|
||||||
|
"description": "Split the project configuration into <projectRoot>/project.json rather than including it inside workspace.json",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
}
|
||||||
16
packages/react-native/src/utils/format-file.ts
Normal file
16
packages/react-native/src/utils/format-file.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { stripIndents } from '@nrwl/devkit';
|
||||||
|
import { format } from 'prettier';
|
||||||
|
|
||||||
|
export function formatFile(content, ...values) {
|
||||||
|
return format(
|
||||||
|
stripIndents(content, values)
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.join('')
|
||||||
|
.trim(),
|
||||||
|
{
|
||||||
|
singleQuote: true,
|
||||||
|
parser: 'typescript',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ export const reactNativeCommunityCliIos = '6.2.0';
|
|||||||
export const reactNativeCommunityCliAndroid = '6.3.0';
|
export const reactNativeCommunityCliAndroid = '6.3.0';
|
||||||
|
|
||||||
export const reactNativeConfigVersion = '1.4.5';
|
export const reactNativeConfigVersion = '1.4.5';
|
||||||
|
export const reactNativeAsyncStorageVersion = '1.15.17';
|
||||||
|
|
||||||
export const metroReactNativeBabelPresetVersion = '0.66.2';
|
export const metroReactNativeBabelPresetVersion = '0.66.2';
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
"storybook-configuration": {
|
"storybook-configuration": {
|
||||||
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationSchematic",
|
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationSchematic",
|
||||||
"schema": "./src/generators/storybook-configuration/schema.json",
|
"schema": "./src/generators/storybook-configuration/schema.json",
|
||||||
"description": "Set up storybook for a react library",
|
"description": "Set up storybook for a react app or library",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -65,7 +65,7 @@
|
|||||||
"stories": {
|
"stories": {
|
||||||
"factory": "./src/generators/stories/stories#storiesSchematic",
|
"factory": "./src/generators/stories/stories#storiesSchematic",
|
||||||
"schema": "./src/generators/stories/schema.json",
|
"schema": "./src/generators/stories/schema.json",
|
||||||
"description": "Create stories/specs for all components declared in a library",
|
"description": "Create stories/specs for all components declared in an app or library",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -125,7 +125,7 @@
|
|||||||
"storybook-configuration": {
|
"storybook-configuration": {
|
||||||
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationGenerator",
|
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationGenerator",
|
||||||
"schema": "./src/generators/storybook-configuration/schema.json",
|
"schema": "./src/generators/storybook-configuration/schema.json",
|
||||||
"description": "Set up storybook for a react library",
|
"description": "Set up storybook for a react app or library",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -146,7 +146,7 @@
|
|||||||
"stories": {
|
"stories": {
|
||||||
"factory": "./src/generators/stories/stories#storiesGenerator",
|
"factory": "./src/generators/stories/stories#storiesGenerator",
|
||||||
"schema": "./src/generators/stories/schema.json",
|
"schema": "./src/generators/stories/schema.json",
|
||||||
"description": "Create stories/specs for all components declared in a library",
|
"description": "Create stories/specs for all components declared in an app or library",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -41,9 +41,6 @@ export function createComponentStoriesFile(
|
|||||||
const proj = getProjects(host).get(project);
|
const proj = getProjects(host).get(project);
|
||||||
const sourceRoot = proj.sourceRoot;
|
const sourceRoot = proj.sourceRoot;
|
||||||
|
|
||||||
// TODO: Remove this entirely, given we don't support TSLint with React?
|
|
||||||
const usesEsLint = true;
|
|
||||||
|
|
||||||
const componentFilePath = joinPathFragments(sourceRoot, componentPath);
|
const componentFilePath = joinPathFragments(sourceRoot, componentPath);
|
||||||
|
|
||||||
const componentDirectory = componentFilePath.replace(
|
const componentDirectory = componentFilePath.replace(
|
||||||
@ -132,7 +129,6 @@ export function createComponentStoriesFile(
|
|||||||
componentName: (cmpDeclaration as any).name.text,
|
componentName: (cmpDeclaration as any).name.text,
|
||||||
isPlainJs,
|
isPlainJs,
|
||||||
fileExt,
|
fileExt,
|
||||||
usesEsLint,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,8 @@ export interface CommonNxStorybookConfig {
|
|||||||
| '@storybook/web-components'
|
| '@storybook/web-components'
|
||||||
| '@storybook/vue'
|
| '@storybook/vue'
|
||||||
| '@storybook/vue3'
|
| '@storybook/vue3'
|
||||||
| '@storybook/svelte';
|
| '@storybook/svelte'
|
||||||
|
| '@storybook/react-native';
|
||||||
projectBuildConfig?: string;
|
projectBuildConfig?: string;
|
||||||
config: StorybookConfig;
|
config: StorybookConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
resolveCommonStorybookOptionMapper,
|
resolveCommonStorybookOptionMapper,
|
||||||
runStorybookSetupCheck,
|
runStorybookSetupCheck,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
export interface StorybookExecutorOptions extends CommonNxStorybookConfig {
|
export interface StorybookExecutorOptions extends CommonNxStorybookConfig {
|
||||||
host?: string;
|
host?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
@ -23,7 +22,7 @@ export interface StorybookExecutorOptions extends CommonNxStorybookConfig {
|
|||||||
export default async function* storybookExecutor(
|
export default async function* storybookExecutor(
|
||||||
options: StorybookExecutorOptions,
|
options: StorybookExecutorOptions,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
) {
|
): AsyncGenerator<{ success: boolean }> {
|
||||||
let frameworkPath = getStorybookFrameworkPath(options.uiFramework);
|
let frameworkPath = getStorybookFrameworkPath(options.uiFramework);
|
||||||
|
|
||||||
const frameworkOptions = (await import(frameworkPath)).default;
|
const frameworkOptions = (await import(frameworkPath)).default;
|
||||||
|
|||||||
@ -82,7 +82,9 @@ export async function configurationGenerator(
|
|||||||
return runTasksInSerial(...tasks);
|
return runTasksInSerial(...tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSchema(schema: StorybookConfigureSchema) {
|
function normalizeSchema(
|
||||||
|
schema: StorybookConfigureSchema
|
||||||
|
): StorybookConfigureSchema {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
configureCypress: true,
|
configureCypress: true,
|
||||||
linter: Linter.TsLint,
|
linter: Linter.TsLint,
|
||||||
@ -209,14 +211,19 @@ function configureTsProjectConfig(
|
|||||||
tsConfigContent = readJson<TsConfig>(tree, tsConfigPath);
|
tsConfigContent = readJson<TsConfig>(tree, tsConfigPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
tsConfigContent.exclude = [
|
if (
|
||||||
...(tsConfigContent.exclude || []),
|
!tsConfigContent.exclude.includes('**/*.stories.ts') &&
|
||||||
'**/*.stories.ts',
|
!tsConfigContent.exclude.includes('**/*.stories.js')
|
||||||
'**/*.stories.js',
|
) {
|
||||||
...(isFramework('react', schema)
|
tsConfigContent.exclude = [
|
||||||
? ['**/*.stories.jsx', '**/*.stories.tsx']
|
...(tsConfigContent.exclude || []),
|
||||||
: []),
|
'**/*.stories.ts',
|
||||||
];
|
'**/*.stories.js',
|
||||||
|
...(isFramework('react', schema) || isFramework('react-native', schema)
|
||||||
|
? ['**/*.stories.jsx', '**/*.stories.tsx']
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
writeJson(tree, tsConfigPath, tsConfigContent);
|
writeJson(tree, tsConfigPath, tsConfigContent);
|
||||||
}
|
}
|
||||||
@ -231,12 +238,18 @@ function configureTsSolutionConfig(
|
|||||||
const tsConfigPath = join(root, 'tsconfig.json');
|
const tsConfigPath = join(root, 'tsconfig.json');
|
||||||
const tsConfigContent = readJson<TsConfig>(tree, tsConfigPath);
|
const tsConfigContent = readJson<TsConfig>(tree, tsConfigPath);
|
||||||
|
|
||||||
tsConfigContent.references = [
|
if (
|
||||||
...(tsConfigContent.references || []),
|
!tsConfigContent.references
|
||||||
{
|
.map((reference) => reference.path)
|
||||||
path: './.storybook/tsconfig.json',
|
.includes('./.storybook/tsconfig.json')
|
||||||
},
|
) {
|
||||||
];
|
tsConfigContent.references = [
|
||||||
|
...(tsConfigContent.references || []),
|
||||||
|
{
|
||||||
|
path: './.storybook/tsconfig.json',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
writeJson(tree, tsConfigPath, tsConfigContent);
|
writeJson(tree, tsConfigPath, tsConfigContent);
|
||||||
}
|
}
|
||||||
@ -305,6 +318,9 @@ function addStorybookTask(
|
|||||||
uiFramework: string,
|
uiFramework: string,
|
||||||
buildTargetForAngularProjects: string
|
buildTargetForAngularProjects: string
|
||||||
) {
|
) {
|
||||||
|
if (uiFramework === '@storybook/react-native') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const projectConfig = readProjectConfiguration(tree, projectName);
|
const projectConfig = readProjectConfiguration(tree, projectName);
|
||||||
projectConfig.targets['storybook'] = {
|
projectConfig.targets['storybook'] = {
|
||||||
executor: '@nrwl/storybook:storybook',
|
executor: '@nrwl/storybook:storybook',
|
||||||
|
|||||||
@ -11,7 +11,7 @@ module.exports = {
|
|||||||
'../src/<%= projectType %>/**/*.stories.mdx',
|
'../src/<%= projectType %>/**/*.stories.mdx',
|
||||||
'../src/<%= projectType %>/**/*.stories.@(js|jsx|ts|tsx)'
|
'../src/<%= projectType %>/**/*.stories.@(js|jsx|ts|tsx)'
|
||||||
],
|
],
|
||||||
addons: [...rootMain.addons <% if(uiFramework === '@storybook/react') { %>, '@nrwl/react/plugins/storybook' <% } %>],
|
addons: [...rootMain.addons <% if(uiFramework === '@storybook/react') { %>, '@nrwl/react/plugins/storybook' <% } %><% if(uiFramework === '@storybook/react-native') { %>, '@storybook/addon-ondevice-actions', '@storybook/addon-ondevice-backgrounds', '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-notes' <% } %>],
|
||||||
webpackFinal: async (config, { configType }) => {
|
webpackFinal: async (config, { configType }) => {
|
||||||
// apply any global webpack configs that might have been specified in .storybook/main.js
|
// apply any global webpack configs that might have been specified in .storybook/main.js
|
||||||
if (rootMain.webpackFinal) {
|
if (rootMain.webpackFinal) {
|
||||||
|
|||||||
@ -10,5 +10,5 @@
|
|||||||
"<%= offsetFromRoot %>../node_modules/@nrwl/react/typings/image.d.ts"
|
"<%= offsetFromRoot %>../node_modules/@nrwl/react/typings/image.d.ts"
|
||||||
],<% } %>
|
],<% } %>
|
||||||
"exclude": ["../**/*.spec.ts" <% if(uiFramework === '@storybook/react') { %>, "../**/*.spec.js", "../**/*.spec.tsx", "../**/*.spec.jsx"<% } %>],
|
"exclude": ["../**/*.spec.ts" <% if(uiFramework === '@storybook/react') { %>, "../**/*.spec.js", "../**/*.spec.tsx", "../**/*.spec.jsx"<% } %>],
|
||||||
"include": ["../src/**/*", "*.js"]
|
"include": ["../src/**/*", "*.js" <% if(uiFramework === '@storybook/react-native') { %>, "*.ts", "*.tsx"<% } %>]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import { Linter } from '@nrwl/linter';
|
|||||||
|
|
||||||
export interface StorybookConfigureSchema {
|
export interface StorybookConfigureSchema {
|
||||||
name: string;
|
name: string;
|
||||||
uiFramework: '@storybook/angular' | '@storybook/react';
|
uiFramework:
|
||||||
|
| '@storybook/angular'
|
||||||
|
| '@storybook/react'
|
||||||
|
| '@storybook/react-native';
|
||||||
configureCypress?: boolean;
|
configureCypress?: boolean;
|
||||||
linter?: Linter;
|
linter?: Linter;
|
||||||
js?: boolean;
|
js?: boolean;
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import {
|
|||||||
babelLoaderVersion,
|
babelLoaderVersion,
|
||||||
babelPresetTypescriptVersion,
|
babelPresetTypescriptVersion,
|
||||||
nxVersion,
|
nxVersion,
|
||||||
|
reactNativeStorybookLoader,
|
||||||
|
storybookReactNativeVersion,
|
||||||
storybookVersion,
|
storybookVersion,
|
||||||
svgrVersion,
|
svgrVersion,
|
||||||
urlLoaderVersion,
|
urlLoaderVersion,
|
||||||
@ -100,6 +102,7 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
|
|||||||
devDependencies['@storybook/manager-webpack5'] = storybookVersion;
|
devDependencies['@storybook/manager-webpack5'] = storybookVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFramework('html', schema)) {
|
if (isFramework('html', schema)) {
|
||||||
devDependencies['@storybook/html'] = storybookVersion;
|
devDependencies['@storybook/html'] = storybookVersion;
|
||||||
}
|
}
|
||||||
@ -120,6 +123,55 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
|
|||||||
devDependencies['@storybook/svelte'] = storybookVersion;
|
devDependencies['@storybook/svelte'] = storybookVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFramework('react-native', schema)) {
|
||||||
|
if (
|
||||||
|
!packageJson.dependencies['@storybook/react-native'] &&
|
||||||
|
!packageJson.devDependencies['@storybook/react-native']
|
||||||
|
) {
|
||||||
|
devDependencies['@storybook/react-native'] = storybookReactNativeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!packageJson.dependencies['@storybook/addon-ondevice-actions'] &&
|
||||||
|
!packageJson.devDependencies['@storybook/addon-ondevice-actions']
|
||||||
|
) {
|
||||||
|
devDependencies['@storybook/addon-ondevice-actions'] =
|
||||||
|
storybookReactNativeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!packageJson.dependencies['@storybook/addon-ondevice-backgrounds'] &&
|
||||||
|
!packageJson.devDependencies['@storybook/addon-ondevice-backgrounds']
|
||||||
|
) {
|
||||||
|
devDependencies['@storybook/addon-ondevice-backgrounds'] =
|
||||||
|
storybookReactNativeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!packageJson.dependencies['@storybook/addon-ondevice-controls'] &&
|
||||||
|
!packageJson.devDependencies['@storybook/addon-ondevice-controls']
|
||||||
|
) {
|
||||||
|
devDependencies['@storybook/addon-ondevice-controls'] =
|
||||||
|
storybookReactNativeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!packageJson.dependencies['@storybook/addon-ondevice-notes'] &&
|
||||||
|
!packageJson.devDependencies['@storybook/addon-ondevice-notes']
|
||||||
|
) {
|
||||||
|
devDependencies['@storybook/addon-ondevice-notes'] =
|
||||||
|
storybookReactNativeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!packageJson.dependencies['react-native-storybook-loader'] &&
|
||||||
|
!packageJson.devDependencies['react-native-storybook-loader']
|
||||||
|
) {
|
||||||
|
devDependencies['react-native-storybook-loader'] =
|
||||||
|
reactNativeStorybookLoader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return addDependenciesToPackageJson(host, dependencies, devDependencies);
|
return addDependenciesToPackageJson(host, dependencies, devDependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,6 @@ export interface Schema {
|
|||||||
| '@storybook/web-components'
|
| '@storybook/web-components'
|
||||||
| '@storybook/vue'
|
| '@storybook/vue'
|
||||||
| '@storybook/vue3'
|
| '@storybook/vue3'
|
||||||
| '@storybook/svelte';
|
| '@storybook/svelte'
|
||||||
|
| '@storybook/react-native';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,8 @@
|
|||||||
"@storybook/web-components",
|
"@storybook/web-components",
|
||||||
"@storybook/vue",
|
"@storybook/vue",
|
||||||
"@storybook/vue3",
|
"@storybook/vue3",
|
||||||
"@storybook/svelte"
|
"@storybook/svelte",
|
||||||
|
"@storybook/react-native"
|
||||||
],
|
],
|
||||||
"x-prompt": "What UI framework plugin should storybook use?"
|
"x-prompt": "What UI framework plugin should storybook use?"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,11 +131,19 @@ function maybeUpdateVersion(tree: Tree): GeneratorCallback {
|
|||||||
|
|
||||||
const allStorybookPackagesInDependencies = Object.keys(
|
const allStorybookPackagesInDependencies = Object.keys(
|
||||||
json.dependencies
|
json.dependencies
|
||||||
).filter((packageName: string) => packageName.startsWith('@storybook/'));
|
).filter(
|
||||||
|
(packageName: string) =>
|
||||||
|
packageName.startsWith('@storybook/') &&
|
||||||
|
!packageName.includes('@storybook/react-native')
|
||||||
|
);
|
||||||
|
|
||||||
const allStorybookPackagesInDevDependencies = Object.keys(
|
const allStorybookPackagesInDevDependencies = Object.keys(
|
||||||
json.devDependencies
|
json.devDependencies
|
||||||
).filter((packageName: string) => packageName.startsWith('@storybook/'));
|
).filter(
|
||||||
|
(packageName: string) =>
|
||||||
|
packageName.startsWith('@storybook/') &&
|
||||||
|
!packageName.includes('@storybook/react-native')
|
||||||
|
);
|
||||||
|
|
||||||
const storybookPackages = [
|
const storybookPackages = [
|
||||||
...allStorybookPackagesInDependencies,
|
...allStorybookPackagesInDependencies,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export const Constants = {
|
|||||||
vue: '@storybook/vue',
|
vue: '@storybook/vue',
|
||||||
vue3: '@storybook/vue3',
|
vue3: '@storybook/vue3',
|
||||||
svelte: '@storybook/svelte',
|
svelte: '@storybook/svelte',
|
||||||
|
'react-native': '@storybook/react-native',
|
||||||
} as const,
|
} as const,
|
||||||
};
|
};
|
||||||
type Constants = typeof Constants;
|
type Constants = typeof Constants;
|
||||||
@ -63,6 +64,13 @@ export function isFramework(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === 'react-native' &&
|
||||||
|
schema.uiFramework === '@storybook/react-native'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +111,10 @@ function determineStorybookWorkspaceVersion(packageJsonContents) {
|
|||||||
workspaceStorybookVersion =
|
workspaceStorybookVersion =
|
||||||
packageJsonContents['devDependencies']['@storybook/core'];
|
packageJsonContents['devDependencies']['@storybook/core'];
|
||||||
}
|
}
|
||||||
|
if (packageJsonContents['devDependencies']['@storybook/react-native']) {
|
||||||
|
workspaceStorybookVersion =
|
||||||
|
packageJsonContents['dependencies']['@storybook/react-native'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packageJsonContents && packageJsonContents['dependencies']) {
|
if (packageJsonContents && packageJsonContents['dependencies']) {
|
||||||
@ -118,6 +130,10 @@ function determineStorybookWorkspaceVersion(packageJsonContents) {
|
|||||||
workspaceStorybookVersion =
|
workspaceStorybookVersion =
|
||||||
packageJsonContents['dependencies']['@storybook/core'];
|
packageJsonContents['dependencies']['@storybook/core'];
|
||||||
}
|
}
|
||||||
|
if (packageJsonContents['dependencies']['@storybook/react-native']) {
|
||||||
|
workspaceStorybookVersion =
|
||||||
|
packageJsonContents['dependencies']['@storybook/react-native'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceStorybookVersion;
|
return workspaceStorybookVersion;
|
||||||
|
|||||||
@ -5,3 +5,6 @@ export const babelLoaderVersion = '8.1.0';
|
|||||||
export const babelPresetTypescriptVersion = '7.12.13';
|
export const babelPresetTypescriptVersion = '7.12.13';
|
||||||
export const svgrVersion = '^5.4.0';
|
export const svgrVersion = '^5.4.0';
|
||||||
export const urlLoaderVersion = '^3.0.0';
|
export const urlLoaderVersion = '^3.0.0';
|
||||||
|
|
||||||
|
export const storybookReactNativeVersion = '6.0.1-alpha.7';
|
||||||
|
export const reactNativeStorybookLoader = '^2.0.5';
|
||||||
|
|||||||
@ -67,6 +67,7 @@
|
|||||||
"@nrwl/react-native": ["./packages/react-native"],
|
"@nrwl/react-native": ["./packages/react-native"],
|
||||||
"@nrwl/react/*": ["./packages/react/*"],
|
"@nrwl/react/*": ["./packages/react/*"],
|
||||||
"@nrwl/storybook": ["./packages/storybook"],
|
"@nrwl/storybook": ["./packages/storybook"],
|
||||||
|
"@nrwl/storybook/*": ["./packages/storybook/*"],
|
||||||
"@nrwl/tao": ["./packages/tao"],
|
"@nrwl/tao": ["./packages/tao"],
|
||||||
"@nrwl/tao/*": ["./packages/tao/*"],
|
"@nrwl/tao/*": ["./packages/tao/*"],
|
||||||
"@nrwl/typedoc-theme": ["/typedoc-theme/src/index.ts"],
|
"@nrwl/typedoc-theme": ["/typedoc-theme/src/index.ts"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user