fix(react-native): fix react native storybook for lib (#29210)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes https://github.com/nrwl/nx/issues/28802
This commit is contained in:
Emily Xiong 2024-12-11 12:01:17 -08:00 committed by GitHub
parent c2eae0e297
commit b4aadccac8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 174 additions and 32 deletions

View File

@ -93,7 +93,7 @@
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"default": "vite",
"x-priority": "important"
}
},

View File

@ -73,6 +73,7 @@
}
},
"required": ["project"],
"examplesFile": "This generator will set up Storybook for your **React Native** project.\n\n```bash\nnx g @nx/react-native:storybook-configuration project-name\n```\n\nWhen running this generator, you will be prompted to provide the following:\n\n- The `name` of the project you want to generate the configuration for.\n- Whether you want to set up [Storybook interaction tests](https://storybook.js.org/docs/react/writing-tests/interaction-testing) (`interactionTests`). If you choose `yes`, a `play` function will be added to your stories, and all the necessary dependencies will be installed. Also, a `test-storybook` target will be generated in your project's `project.json`, with a command to invoke the [Storybook `test-runner`](https://storybook.js.org/docs/react/writing-tests/test-runner). You can read more about this in the [Nx Storybook interaction tests documentation page](/recipes/storybook/storybook-interaction-tests#setup-storybook-interaction-tests)..\n- Whether you want to `generateStories` for the components in your project. If you choose `yes`, a `.stories.ts` file will be generated next to each of your components in your project.\n\nYou must provide a `name` for the generator to work.\n\nBy default, this generator will also set up [Storybook interaction tests](https://storybook.js.org/docs/react/writing-tests/interaction-testing). If you don't want to set up Storybook interaction tests, you can pass the `--interactionTests=false` option, but it's not recommended.\n\nThere are a number of other options available. Let's take a look at some examples.\n\n## Examples\n\n### Generate Storybook configuration\n\n```bash\nnx g @nx/react-native:storybook-configuration ui\n```\n\nThis will generate Storybook configuration for the `ui` project using TypeScript for the Storybook configuration files (the files inside the `.storybook` directory, eg. `.storybook/main.ts`).\n\n### Ignore certain paths when generating stories\n\n```bash\nnx g @nx/react-native:storybook-configuration ui --generateStories=true --ignorePaths=libs/ui/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts\n```\n\nThis will generate a Storybook configuration for the `ui` project and generate stories for all components in the `libs/ui/src/lib` directory, except for the ones in the `libs/ui/src/not-stories` directory, and the ones in the `apps/my-app` directory that end with `.something.ts`, and also for components that their file name is of the pattern `*.other.*`.\n\nThis is useful if you have a project that contains components that are not meant to be used in isolation, but rather as part of a larger component.\n\nBy default, Nx will ignore the following paths:\n\n```text\n*.stories.ts, *.stories.tsx, *.stories.js, *.stories.jsx, *.stories.mdx\n```\n\nbut you can change this behaviour easily, as explained above.\n\n### Generate stories using JavaScript instead of TypeScript\n\n```bash\nnx g @nx/react-native:storybook-configuration ui --generateStories=true --js=true\n```\n\nThis will generate stories for all the components in the `ui` project using JavaScript instead of TypeScript. So, you will have `.stories.js` files next to your components.\n\n### Generate Storybook configuration using JavaScript\n\n```bash\nnx g @nx/react-native:storybook-configuration ui --tsConfiguration=false\n```\n\nBy default, our generator generates TypeScript Storybook configuration files. You can choose to use JavaScript for the Storybook configuration files of your project (the files inside the `.storybook` directory, eg. `.storybook/main.js`).\n",
"presets": []
},
"description": "Set up Storybook for a React Native application or library.",

View File

@ -34,7 +34,7 @@
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"default": "vite",
"x-priority": "important"
}
},

View File

@ -9,21 +9,42 @@ import {
fileExists,
checkFilesExist,
runE2ETests,
updateFile,
} from 'e2e/utils';
describe('@nx/react-native', () => {
let proj: string;
let appName: string;
let libName: string;
let componentName: string;
beforeAll(() => {
newProject();
proj = newProject();
appName = uniq('app');
runCLI(
`generate @nx/react-native:app ${appName} --install=false --no-interactive --unitTestRunner=jest --linter=eslint`
);
libName = uniq('lib');
runCLI(
`generate @nx/react-native:lib ${libName} --buildable --no-interactive --unitTestRunner=jest --linter=eslint`
);
componentName = uniq('Component');
runCLI(
`generate @nx/react-native:component ${libName}/src/lib/${componentName}/${componentName} --export --no-interactive`
);
updateFile(`${appName}/src/app/App.tsx`, (content) => {
let updated = `// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport {${componentName}} from '${proj}/${libName}';\n${content}`;
return updated;
});
});
afterAll(() => cleanupProject());
it('should test and lint', async () => {
expect(() => runCLI(`test ${appName}`)).not.toThrow();
expect(() => runCLI(`lint ${appName}`)).not.toThrow();
});
it('should bundle the app', async () => {
expect(() =>
runCLI(
@ -104,12 +125,38 @@ describe('@nx/react-native', () => {
it('should create storybook with application', async () => {
runCLI(
`generate @nx/react:storybook-configuration ${appName} --generateStories --no-interactive`
`generate @nx/react-native:storybook-configuration ${appName} --generateStories --no-interactive`
);
checkFilesExist(
`${appName}/.storybook/main.ts`,
`${appName}/src/app/App.stories.tsx`
);
runCLI(`build-storybook ${appName}`);
checkFilesExist(`${appName}/storybook-static/index.html`);
});
it('should build publishable library', async () => {
expect(() => {
runCLI(`build ${libName}`);
checkFilesExist(
`dist/${libName}/index.esm.js`,
`dist/${libName}/src/index.d.ts`
);
}).not.toThrow();
});
it('should create storybook with library', async () => {
runCLI(
`generate @nx/react-native:storybook-configuration ${libName} --generateStories --no-interactive`
);
checkFilesExist(
`${libName}/.storybook/main.ts`,
`${libName}/src/lib/${componentName}/${componentName}.stories.tsx`
);
runCLI(`build-storybook ${libName}`);
checkFilesExist(`${libName}/storybook-static/index.html`);
});
it('should run build with vite bundler and e2e with playwright', async () => {
@ -137,5 +184,7 @@ describe('@nx/react-native', () => {
`apps/${appName2}/.storybook/main.ts`,
`apps/${appName2}/src/app/App.stories.tsx`
);
runCLI(`build-storybook ${appName2}`);
checkFilesExist(`apps/${appName2}/storybook-static/index.html`);
});
});

View File

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

View File

@ -59,7 +59,10 @@ export const createNodesV2: CreateNodesV2<ReactNativePluginOptions> = [
'**/app.{json,config.js,config.ts}',
async (configFiles, options, context) => {
const optionsHash = hashObject(options);
const cachePath = join(workspaceDataDirectory, `expo-${optionsHash}.hash`);
const cachePath = join(
workspaceDataDirectory,
`react-native-${optionsHash}.hash`
);
const targetsCache = readTargetsCache(cachePath);
try {

View File

@ -93,7 +93,7 @@
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"default": "vite",
"x-priority": "important"
}
},

View File

@ -75,5 +75,6 @@
]
}
},
"required": ["project"]
"required": ["project"],
"examplesFile": "../../../docs/storybook-configuration-examples.md"
}

View File

@ -30,7 +30,7 @@ const rollupPlugin = (matchers: RegExp[]) => ({
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/<%= fileName %>',
cacheDir: '<%= offsetFromRoot %>node_modules/.vite/<%= projectRoot %>',
define: {
global: 'window',
},
@ -43,7 +43,7 @@ export default defineConfig({
build: {
reportCompressedSize: true,
commonjsOptions: { transformMixedEsModules: true },
outDir: '../../dist/apps/<%= fileName %>/web',
outDir: '<%= offsetFromRoot %>dist/<%= projectRoot %>/web',
rollupOptions: {
plugins: [rollupPlugin([/react-native-vector-icons/])],
},

View File

@ -34,7 +34,7 @@
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"default": "vite",
"x-priority": "important"
}
},

View File

@ -37,6 +37,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
tsConfigPaths: options.tsConfigPaths,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
setParserOptionsProject: options.setParserOptionsProject,
addPlugin: options.addPlugin,
});
@ -69,7 +70,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) {
}
if (!options.skipPackageJson) {
const installTask = await addDependenciesToPackageJson(
const installTask = addDependenciesToPackageJson(
host,
extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies

View File

@ -25,9 +25,9 @@ import {
createProjectStorybookDir,
createStorybookTsconfigFile,
editTsconfigBaseJson,
findMetroConfig,
findNextConfig,
findViteConfig,
isUsingReactNative,
projectIsRootProjectInStandaloneWorkspace,
updateLintConfig,
} from './lib/util-functions';
@ -82,7 +82,6 @@ export async function configurationGeneratorInternal(
const viteConfigFilePath = viteConfig?.fullConfigPath;
const viteConfigFileName = viteConfig?.viteConfigFileName;
const nextConfigFilePath = findNextConfig(tree, root);
const metroConfigFilePath = findMetroConfig(tree, root);
if (viteConfigFilePath) {
if (schema.uiFramework === '@storybook/react-webpack5') {
@ -133,7 +132,7 @@ export async function configurationGeneratorInternal(
const usesVite =
!!viteConfigFilePath || schema.uiFramework?.endsWith('-vite');
const useReactNative = !!metroConfigFilePath;
const usesReactNative = isUsingReactNative(schema.project);
createProjectStorybookDir(
tree,
@ -152,7 +151,7 @@ export async function configurationGeneratorInternal(
viteConfigFilePath,
hasPlugin,
viteConfigFileName,
useReactNative
usesReactNative
);
if (schema.uiFramework !== '@storybook/angular') {

View File

@ -4,6 +4,7 @@ import {
joinPathFragments,
logger,
offsetFromRoot,
readCachedProjectGraph,
readJson,
readNxJson,
readProjectConfiguration,
@ -577,7 +578,7 @@ export function createProjectStorybookDir(
viteConfigFilePath?: string,
hasPlugin?: boolean,
viteConfigFileName?: string,
useReactNative?: boolean
usesReactNative?: boolean
) {
let projectDirectory =
projectType === 'application'
@ -622,7 +623,7 @@ export function createProjectStorybookDir(
viteConfigFilePath,
hasPlugin,
viteConfigFileName,
useReactNative,
usesReactNative,
});
if (js) {
@ -739,13 +740,14 @@ export function findNextConfig(
}
}
export function findMetroConfig(
tree: Tree,
projectRoot: string
): string | undefined {
const nextConfigPath = joinPathFragments(projectRoot, `metro.config.js`);
if (tree.exists(nextConfigPath)) {
return nextConfigPath;
export function isUsingReactNative(projectName: string): boolean {
try {
const projectGraph = readCachedProjectGraph();
return projectGraph?.dependencies?.[projectName]?.some(
(dep) => dep.target === 'npm:react-native'
);
} catch {
return false;
}
}

View File

@ -25,7 +25,7 @@ const config: StorybookConfig = {
<% } %>
},
},
<% if (useReactNative && uiFramework === '@storybook/react-webpack5') { %>webpackFinal: async (config) => {
<% if (usesReactNative && uiFramework === '@storybook/react-webpack5') { %>webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
@ -43,6 +43,21 @@ const config: StorybookConfig = {
},<% } %><% if (usesVite && !viteConfigFilePath) { %>
viteFinal: async (config) =>
mergeConfig(config, {
<% if (usesReactNative) { %>define: {
global: 'window',
},
resolve: {
extensions: [
'.web.tsx',
'.web.ts',
'.web.jsx',
'.web.js',
...(config.resolve?.extensions ?? []),
],
alias: {
'react-native': 'react-native-web',
},
},<% } %>
plugins: [<% if(uiFramework === '@storybook/vue3-vite') { %>vue(), <% } %><% if(uiFramework === '@storybook/react-vite') { %>react(), <% } %>nxViteTsPaths()],
}),
<% } %>

View File

@ -25,7 +25,7 @@ const config = {
<% } %>
},
},
<% if (useReactNative && uiFramework === '@storybook/react-webpack5') { %>webpackFinal: async (config) => {
<% if (usesReactNative && uiFramework === '@storybook/react-webpack5') { %>webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
@ -43,6 +43,21 @@ const config = {
},<% } %><% if (usesVite && !viteConfigFilePath) { %>
viteFinal: async (config) =>
mergeConfig(config, {
<% if (usesReactNative) { %>define: {
global: 'window',
},
resolve: {
extensions: [
'.web.tsx',
'.web.ts',
'.web.jsx',
'.web.js',
...(config.resolve.extensions ?? []),
],
alias: {
'react-native': 'react-native-web',
},
},<% } %>
plugins: [<% if(uiFramework === '@storybook/vue3-vite') { %>vue(), <% } %>nxViteTsPaths()],
}),
<% } %>

View File

@ -1,9 +1,4 @@
import {
TargetConfiguration,
Tree,
readNxJson,
updateNxJson,
} from '@nx/devkit';
import { TargetConfiguration, Tree } from '@nx/devkit';
import { CompilerOptions } from 'typescript';
import { statSync } from 'fs';
import { findNodes } from '@nx/js';