From e78319af43023f83213bee3917d7b1f6aafe3fb3 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Thu, 5 Dec 2024 01:54:47 -0500 Subject: [PATCH] fix(react-native): fix react native storybook for lib --- .../react-native/generators/application.json | 2 +- .../generators/storybook-configuration.json | 1 + .../generators/web-configuration.json | 2 +- e2e/react-native/src/react-native.test.ts | 53 +++++++++++++++- .../docs/storybook-configuration-examples.md | 61 +++++++++++++++++++ packages/react-native/plugins/plugin.ts | 5 +- .../src/generators/application/schema.json | 2 +- .../storybook-configuration/schema.json | 3 +- .../files/base-vite/vite.config.ts__tmpl__ | 4 +- .../generators/web-configuration/schema.json | 2 +- .../react-native/src/utils/add-linting.ts | 3 +- .../src/generators/library/library.spec.ts | 1 + .../generators/configuration/configuration.ts | 7 +-- .../configuration/lib/util-functions.ts | 20 +++--- .../.storybook/main.ts__tmpl__ | 17 +++++- .../project-files/.storybook/main.js__tmpl__ | 17 +++++- packages/storybook/src/utils/utilities.ts | 7 +-- 17 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 packages/react-native/docs/storybook-configuration-examples.md diff --git a/docs/generated/packages/react-native/generators/application.json b/docs/generated/packages/react-native/generators/application.json index 4100814e2fc7c2..658f7902791032 100644 --- a/docs/generated/packages/react-native/generators/application.json +++ b/docs/generated/packages/react-native/generators/application.json @@ -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" } }, diff --git a/docs/generated/packages/react-native/generators/storybook-configuration.json b/docs/generated/packages/react-native/generators/storybook-configuration.json index e8233f36189fb7..f213f58be63767 100644 --- a/docs/generated/packages/react-native/generators/storybook-configuration.json +++ b/docs/generated/packages/react-native/generators/storybook-configuration.json @@ -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.", diff --git a/docs/generated/packages/react-native/generators/web-configuration.json b/docs/generated/packages/react-native/generators/web-configuration.json index d472267f6b4ba9..ed8b4b6de0b780 100644 --- a/docs/generated/packages/react-native/generators/web-configuration.json +++ b/docs/generated/packages/react-native/generators/web-configuration.json @@ -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" } }, diff --git a/e2e/react-native/src/react-native.test.ts b/e2e/react-native/src/react-native.test.ts index 544c01d279ef20..1cacffc310a788 100644 --- a/e2e/react-native/src/react-native.test.ts +++ b/e2e/react-native/src/react-native.test.ts @@ -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`); }); }); diff --git a/packages/react-native/docs/storybook-configuration-examples.md b/packages/react-native/docs/storybook-configuration-examples.md new file mode 100644 index 00000000000000..e7a2480512f54d --- /dev/null +++ b/packages/react-native/docs/storybook-configuration-examples.md @@ -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`). diff --git a/packages/react-native/plugins/plugin.ts b/packages/react-native/plugins/plugin.ts index 5bb68b7ac43812..470195afef7eb5 100644 --- a/packages/react-native/plugins/plugin.ts +++ b/packages/react-native/plugins/plugin.ts @@ -59,7 +59,10 @@ export const createNodesV2: CreateNodesV2 = [ '**/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 { diff --git a/packages/react-native/src/generators/application/schema.json b/packages/react-native/src/generators/application/schema.json index 8bbc3a51c112ec..e6f1384e2d6407 100644 --- a/packages/react-native/src/generators/application/schema.json +++ b/packages/react-native/src/generators/application/schema.json @@ -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" } }, diff --git a/packages/react-native/src/generators/storybook-configuration/schema.json b/packages/react-native/src/generators/storybook-configuration/schema.json index d12db951ea77b6..07e5de252b53a6 100644 --- a/packages/react-native/src/generators/storybook-configuration/schema.json +++ b/packages/react-native/src/generators/storybook-configuration/schema.json @@ -75,5 +75,6 @@ ] } }, - "required": ["project"] + "required": ["project"], + "examplesFile": "../../../docs/storybook-configuration-examples.md" } diff --git a/packages/react-native/src/generators/web-configuration/files/base-vite/vite.config.ts__tmpl__ b/packages/react-native/src/generators/web-configuration/files/base-vite/vite.config.ts__tmpl__ index 1514782bd9dee9..c7ebe41a52334e 100644 --- a/packages/react-native/src/generators/web-configuration/files/base-vite/vite.config.ts__tmpl__ +++ b/packages/react-native/src/generators/web-configuration/files/base-vite/vite.config.ts__tmpl__ @@ -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/])], }, diff --git a/packages/react-native/src/generators/web-configuration/schema.json b/packages/react-native/src/generators/web-configuration/schema.json index a355fef9044b0b..18dc91d089c29e 100644 --- a/packages/react-native/src/generators/web-configuration/schema.json +++ b/packages/react-native/src/generators/web-configuration/schema.json @@ -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" } }, diff --git a/packages/react-native/src/utils/add-linting.ts b/packages/react-native/src/utils/add-linting.ts index 0b340d04f0210e..ccb56e9ea0d4d0 100644 --- a/packages/react-native/src/utils/add-linting.ts +++ b/packages/react-native/src/utils/add-linting.ts @@ -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 diff --git a/packages/react/src/generators/library/library.spec.ts b/packages/react/src/generators/library/library.spec.ts index 99e04801a0ed9e..1f44a2e982cadb 100644 --- a/packages/react/src/generators/library/library.spec.ts +++ b/packages/react/src/generators/library/library.spec.ts @@ -1223,6 +1223,7 @@ module.exports = withNx( }, "type": "module", "types": "./dist/index.esm.d.ts", + "version": "0.0.1", } `); }); diff --git a/packages/storybook/src/generators/configuration/configuration.ts b/packages/storybook/src/generators/configuration/configuration.ts index 8fd73406d42d0d..c7f40e83427ad0 100644 --- a/packages/storybook/src/generators/configuration/configuration.ts +++ b/packages/storybook/src/generators/configuration/configuration.ts @@ -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') { diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index b19b557267fb13..fcee8451260850 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -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; } } diff --git a/packages/storybook/src/generators/configuration/project-files-ts/.storybook/main.ts__tmpl__ b/packages/storybook/src/generators/configuration/project-files-ts/.storybook/main.ts__tmpl__ index 6f8c0abf538e1b..6ac2dada8e0034 100644 --- a/packages/storybook/src/generators/configuration/project-files-ts/.storybook/main.ts__tmpl__ +++ b/packages/storybook/src/generators/configuration/project-files-ts/.storybook/main.ts__tmpl__ @@ -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()], }), <% } %> diff --git a/packages/storybook/src/generators/configuration/project-files/.storybook/main.js__tmpl__ b/packages/storybook/src/generators/configuration/project-files/.storybook/main.js__tmpl__ index f926b900bd43f8..5dae77b4dd12c3 100644 --- a/packages/storybook/src/generators/configuration/project-files/.storybook/main.js__tmpl__ +++ b/packages/storybook/src/generators/configuration/project-files/.storybook/main.js__tmpl__ @@ -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()], }), <% } %> diff --git a/packages/storybook/src/utils/utilities.ts b/packages/storybook/src/utils/utilities.ts index dd7bc3062fa863..bb56e2111bcbcd 100644 --- a/packages/storybook/src/utils/utilities.ts +++ b/packages/storybook/src/utils/utilities.ts @@ -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';