diff --git a/docs/generated/packages/react-native/executors/storybook.json b/docs/generated/packages/react-native/executors/storybook.json index 4352e22efe51b6..629c677ddbe28f 100644 --- a/docs/generated/packages/react-native/executors/storybook.json +++ b/docs/generated/packages/react-native/executors/storybook.json @@ -28,7 +28,8 @@ "silent": { "type": "boolean", "description": "Silences output.", - "default": false + "default": false, + "x-deprecated": "No longer used. It will be silent as default." } }, "required": ["searchDir", "outputFile", "pattern"], diff --git a/packages/expo/src/utils/pod-install-task.ts b/packages/expo/src/utils/pod-install-task.ts index 6aba3b62ede529..9efec97d9d43ca 100644 --- a/packages/expo/src/utils/pod-install-task.ts +++ b/packages/expo/src/utils/pod-install-task.ts @@ -64,13 +64,6 @@ export function podInstall( } logger.info(stdout); if (stdout.includes('Pod installation complete')) { - // Remove build folder after pod install - if (buildFolder) { - buildFolder = join(iosDirectory, buildFolder); - if (existsSync(buildFolder)) { - rmdirSync(buildFolder, { recursive: true }); - } - } resolve(); } else { reject(new Error(podInstallErrorMessage)); diff --git a/packages/react-native/migrations.json b/packages/react-native/migrations.json index 6a2d4b5492ac80..938d7bfe1e352f 100644 --- a/packages/react-native/migrations.json +++ b/packages/react-native/migrations.json @@ -83,6 +83,12 @@ "version": "16.0.0-beta.1", "description": "Replace @nrwl/react-native with @nx/react-native", "implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages" + }, + "update-16-0-2-upgrade-storybook-6-5": { + "cli": "nx", + "version": "16.0.2-beta.0", + "description": "Upgrade @storybook/react-native to 6.5", + "implementation": "./src/migrations/update-16-0-2/upgrade-storybook-6-5" } }, "packageJsonUpdates": { @@ -1291,6 +1297,19 @@ "alwaysAddToPackageJson": false } } + }, + "16.0.2": { + "version": "16.0.2-beta.0", + "packages": { + "@react-native-async-storage/async-storage": { + "version": "1.18.1", + "alwaysAddToPackageJson": false + }, + "react-native-safe-area-context": { + "version": "4.5.1", + "alwaysAddToPackageJson": false + } + } } } } diff --git a/packages/react-native/src/executors/storybook/schema.d.ts b/packages/react-native/src/executors/storybook/schema.d.ts index cf193971d3ac60..8f7e297aab34f7 100644 --- a/packages/react-native/src/executors/storybook/schema.d.ts +++ b/packages/react-native/src/executors/storybook/schema.d.ts @@ -1,7 +1,9 @@ -// options from https://github.com/elderfo/react-native-storybook-loader#options export interface ReactNativeStorybookOptions { searchDir: string[]; outputFile: string; pattern: string; + /** + * @deprecated going to be removed in 17 + */ silent: boolean; } diff --git a/packages/react-native/src/executors/storybook/schema.json b/packages/react-native/src/executors/storybook/schema.json index 961709df7571ce..37c0c9e44b7cb6 100644 --- a/packages/react-native/src/executors/storybook/schema.json +++ b/packages/react-native/src/executors/storybook/schema.json @@ -27,7 +27,8 @@ "silent": { "type": "boolean", "description": "Silences output.", - "default": false + "default": false, + "x-deprecated": "No longer used. It will be silent as default." } }, "required": ["searchDir", "outputFile", "pattern"] diff --git a/packages/react-native/src/executors/storybook/storybook.impl.ts b/packages/react-native/src/executors/storybook/storybook.impl.ts index e4cd0bfecee741..f331ebe43f5ab3 100644 --- a/packages/react-native/src/executors/storybook/storybook.impl.ts +++ b/packages/react-native/src/executors/storybook/storybook.impl.ts @@ -2,17 +2,16 @@ import { join } from 'path'; import { ExecutorContext, logger } from '@nx/devkit'; import { fileExists } from '@nx/workspace/src/utilities/fileutils'; import * as chalk from 'chalk'; +import { sync as globSync } from 'fast-glob'; import { ReactNativeStorybookOptions } from './schema'; -import { ChildProcess, fork } from 'child_process'; import { displayNewlyAddedDepsMessage, syncDeps, } from '../sync-deps/sync-deps.impl'; +import { readFileSync, writeFileSync } from 'fs-extra'; -let childProcess: ChildProcess; - -export default async function* reactNatievStorybookExecutor( +export default async function* reactNativeStorybookExecutor( options: ReactNativeStorybookOptions, context: ExecutorContext ): AsyncGenerator<{ success: boolean }> { @@ -35,6 +34,7 @@ export default async function* reactNatievStorybookExecutor( context.root, context.projectGraph, [ + `@storybook/react-native`, '@storybook/addon-ondevice-actions', '@storybook/addon-ondevice-backgrounds', '@storybook/addon-ondevice-controls', @@ -43,63 +43,28 @@ export default async function* reactNatievStorybookExecutor( ) ); - try { - await runCliStorybook(context.root, options); - yield { success: true }; - } finally { - if (childProcess) { - childProcess.kill(); - } - } + runCliStorybook(context.root, options); + yield { success: true }; } -function runCliStorybook( +export function runCliStorybook( workspaceRoot: 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()); + const storiesFiles: string[] = options.searchDir.flatMap((dir) => + globSync(join(workspaceRoot, dir, options.pattern)) + ); + if (storiesFiles.length === 0) { + logger.warn(`${chalk.bold.yellow('warn')} No stories found.`); + } - childProcess.on('error', (err) => { - reject(err); - }); - childProcess.on('exit', (code) => { - if (code === 0) { - resolve(code); - } else { - reject(code); - } - }); - }); -} + const newContents = `// Auto-generated file created by nx +// DO NOT EDIT. +export function loadStories() { + return [ + ${storiesFiles.map((story) => `require('${story}')`).join(',\n')} + ]; +}`; -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 if (Array.isArray(v)) { - v.forEach((value) => { - acc.push(`--${k}`, value); - }); - } else { - acc.push(`--${k}`, v); - } - return acc; - }, []); + writeFileSync(join(workspaceRoot, options.outputFile), newContents); } diff --git a/packages/react-native/src/generators/component-story/component-story.ts b/packages/react-native/src/generators/component-story/component-story.ts index 19e157ac994166..8f3354fab6f22d 100644 --- a/packages/react-native/src/generators/component-story/component-story.ts +++ b/packages/react-native/src/generators/component-story/component-story.ts @@ -7,37 +7,34 @@ import { normalizePath, Tree, } from '@nx/devkit'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import { findExportDeclarationsForJsx, getComponentNode, - getComponentPropsInterface, } from '@nx/react/src/utils/ast-utils'; -import { CreateComponentStoriesFileSchema } from './schema'; - -export function getArgsDefaultValue(property: ts.SyntaxKind): string { - const typeNameToDefault: Record = { - [ts.SyntaxKind.StringKeyword]: "''", - [ts.SyntaxKind.NumberKeyword]: 0, - [ts.SyntaxKind.BooleanKeyword]: false, - }; - - const resolvedValue = typeNameToDefault[property]; - if (typeof resolvedValue === undefined) { - return "''"; - } else { - return resolvedValue; - } +import { getDefaultsForComponent } from '@nx/react/src/utils/component-props'; +import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; + +let tsModule: typeof import('typescript'); + +export interface CreateComponentStoriesFileSchema { + project: string; + componentPath: string; + skipFormat?: boolean; } export function createComponentStoriesFile( host: Tree, { project, componentPath }: CreateComponentStoriesFileSchema ) { + if (!tsModule) { + tsModule = ensureTypescript(); + } const proj = getProjects(host).get(project); const sourceRoot = proj.sourceRoot; const componentFilePath = joinPathFragments(sourceRoot, componentPath); + const componentDirectory = componentFilePath.replace( componentFilePath.slice(componentFilePath.lastIndexOf('/')), '' @@ -65,10 +62,10 @@ export function createComponentStoriesFile( throw new Error(`Failed to read ${componentFilePath}`); } - const sourceFile = ts.createSourceFile( + const sourceFile = tsModule.createSourceFile( componentFilePath, contents, - ts.ScriptTarget.Latest, + tsModule.ScriptTarget.Latest, true ); @@ -107,7 +104,7 @@ export function createComponentStoriesFile( } } -function findPropsAndGenerateFile( +export function findPropsAndGenerateFile( host: Tree, sourceFile: ts.SourceFile, cmpDeclaration: ts.Node, @@ -117,37 +114,10 @@ function findPropsAndGenerateFile( fileExt: string, fromNodeArray?: boolean ) { - const propsInterface = getComponentPropsInterface(sourceFile, cmpDeclaration); - - 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); - } + const { propsTypeName, props, argTypes } = getDefaultsForComponent( + sourceFile, + cmpDeclaration + ); generateFiles( host, @@ -164,7 +134,6 @@ function findPropsAndGenerateFile( componentName: (cmpDeclaration as any).name.text, isPlainJs, fileExt, - hasActions: argTypes && argTypes.length, } ); } diff --git a/packages/react-native/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ b/packages/react-native/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ index 2243e7611f20a6..b455e56f674f84 100644 --- a/packages/react-native/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ +++ b/packages/react-native/src/generators/component-story/files/__componentFileName__.stories.__fileExt__ @@ -1,24 +1,32 @@ -<% if (hasActions) { %> - import { action } from '@storybook/addon-actions'; -<% } %> -import { storiesOf } from '@storybook/react-native'; -import React from 'react'; -import { - <%= componentName %> - <% if ( propsTypeName ) { %>, <%= propsTypeName %> <% } %> -} from './<%= componentImportFileName %>'; +<% if ( !isPlainJs ) { %>import type { Meta } from '@storybook/react-native';<% } %> +import<% if ( !isPlainJs ) { %> { <% } %> <%= componentName %> <% if ( !isPlainJs ) { %> } <% } %> from './<%= componentImportFileName %>'; -<% if (hasActions) { %> - const actions = {<% for (let argType of argTypes) { %> - <%= argType.name %>: action('<%- argType.actionText %>'), - <% } %>}; +<% if ( isPlainJs ) { %> +export default { + component: <%= componentName %>, + title: '<%= componentName %>',<% if ( argTypes && argTypes.length > 0 ) { %> + argTypes: {<% for (let argType of argTypes) { %> + <%= argType.name %>: { <%- argType.type %> : "<%- argType.actionText %>" },<% } %> +} + <% } %> +}; <% } %> -const props <% if ( propsTypeName ) { %>:<%= propsTypeName %><% } %> = {<% for (let prop of props) { %> - <%= prop.name %>: <%- prop.defaultValue %>, -<% } %>}; -storiesOf('<%= componentName %>', module) - .add('Primary', () => ( - <<%= componentName %> {...props} <% if (hasActions) { %> {...actions} <% } %>/> - )); \ No newline at end of file +<% if ( !isPlainJs ) { %> +const Story: Meta> = { + component: <%= componentName %>, + title: '<%= componentName %>',<% if ( argTypes && argTypes.length > 0 ) { %> + argTypes: {<% for (let argType of argTypes) { %> + <%= argType.name %>: { <%- argType.type %> : "<%- argType.actionText %>" },<% } %> +} + <% } %> +}; +export default Story; +<% } %> + +export const Primary = { + args: {<% for (let prop of props) { %> + <%= prop.name %>: <%- prop.defaultValue %>,<% } %> + }, +}; \ No newline at end of file diff --git a/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts b/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts index c2e6b45aa2b23a..391d5c63aed22b 100644 --- a/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts +++ b/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts @@ -20,15 +20,6 @@ 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(() => {}); }); diff --git a/packages/react-native/src/generators/storybook-configuration/configuration.ts b/packages/react-native/src/generators/storybook-configuration/configuration.ts index d9fe8d11f0d865..fe19570e72a2b1 100644 --- a/packages/react-native/src/generators/storybook-configuration/configuration.ts +++ b/packages/react-native/src/generators/storybook-configuration/configuration.ts @@ -1,13 +1,19 @@ import { + addDependenciesToPackageJson, convertNxGenerator, ensurePackage, formatFiles, GeneratorCallback, readProjectConfiguration, + runTasksInSerial, Tree, updateProjectConfiguration, } from '@nx/devkit'; -import { nxVersion } from '../../utils/versions'; +import { + nxVersion, + reactNativeAsyncStorageVersion, + reactNativeSafeAreaContextVersion, +} from '../../utils/versions'; import storiesGenerator from '../stories/stories'; import { addResolverMainFieldsToMetroConfig } from './lib/add-resolver-main-fields-to-metro-config'; @@ -43,6 +49,16 @@ export async function storybookConfigurationGenerator( skipFormat: true, }); + const installRequiredPackagesTask = await addDependenciesToPackageJson( + host, + {}, + { + '@react-native-async-storage/async-storage': + reactNativeAsyncStorageVersion, + 'react-native-safe-area-context': reactNativeSafeAreaContextVersion, + } + ); + addStorybookTask(host, schema.name); createStorybookFiles(host, schema); replaceAppImportWithStorybookToggle(host, schema); @@ -53,7 +69,7 @@ export async function storybookConfigurationGenerator( } await formatFiles(host); - return installTask; + return runTasksInSerial(installTask, installRequiredPackagesTask); } function addStorybookTask(host: Tree, projectName: string) { @@ -61,10 +77,9 @@ function addStorybookTask(host: Tree, projectName: string) { projectConfig.targets['storybook'] = { executor: '@nx/react-native:storybook', options: { - searchDir: [projectConfig.root], - outputFile: './.storybook/story-loader.js', + searchDir: [projectConfig.sourceRoot], + outputFile: './.storybook/story-loader.ts', pattern: '**/*.stories.@(js|jsx|ts|tsx|md)', - silent: false, }, }; diff --git a/packages/react-native/src/generators/storybook-configuration/files/app/storybook.ts.template b/packages/react-native/src/generators/storybook-configuration/files/app/storybook.ts.template index c1291eeb0beea0..c52e78ae8658d3 100644 --- a/packages/react-native/src/generators/storybook-configuration/files/app/storybook.ts.template +++ b/packages/react-native/src/generators/storybook-configuration/files/app/storybook.ts.template @@ -4,6 +4,6 @@ import { loadStories } from '<%= offsetFromRoot %>../.storybook/story-loader'; configure(() => loadStories(), module); -const StorybookUIRoot = getStorybookUI(); +const StorybookUIRoot = getStorybookUI({}); export default StorybookUIRoot; \ No newline at end of file diff --git a/packages/react-native/src/generators/storybook-configuration/files/root/story-loader.js.template b/packages/react-native/src/generators/storybook-configuration/files/root/story-loader.js.template deleted file mode 100644 index 162b6755d2f333..00000000000000 --- a/packages/react-native/src/generators/storybook-configuration/files/root/story-loader.js.template +++ /dev/null @@ -1,13 +0,0 @@ -// 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, -}; diff --git a/packages/react-native/src/generators/storybook-configuration/files/root/story-loader.ts.template b/packages/react-native/src/generators/storybook-configuration/files/root/story-loader.ts.template new file mode 100644 index 00000000000000..e123e03487e917 --- /dev/null +++ b/packages/react-native/src/generators/storybook-configuration/files/root/story-loader.ts.template @@ -0,0 +1,5 @@ +// Auto-generated file created by nx +// DO NOT EDIT. +export function loadStories() { + return []; +} diff --git a/packages/react-native/src/migrations/update-16-0-2/upgrade-storybook-6-5.ts b/packages/react-native/src/migrations/update-16-0-2/upgrade-storybook-6-5.ts new file mode 100644 index 00000000000000..0ea1e59c91b1f9 --- /dev/null +++ b/packages/react-native/src/migrations/update-16-0-2/upgrade-storybook-6-5.ts @@ -0,0 +1,16 @@ +import { Tree, updateJson } from '@nx/devkit'; + +/** + * This migration is to upgrade storybook to 6.5. + * - remove react-native-storybook-loader + * - change .storybook/story-loader.js to .storybook/story-loader.ts + */ +export function update(tree: Tree) { + updateJson(tree, 'package.json', (packageJson) => { + delete packageJson.devDependencies['react-native-storybook-loader']; + return packageJson; + }); + if (tree.exists('.storybook/story-loader.js')) { + tree.rename('.storybook/story-loader.js', '.storybook/story-loader.ts'); + } +} diff --git a/packages/react-native/src/utils/pod-install-task.ts b/packages/react-native/src/utils/pod-install-task.ts index 4c2bd3c5dfcece..bd19965ac01218 100644 --- a/packages/react-native/src/utils/pod-install-task.ts +++ b/packages/react-native/src/utils/pod-install-task.ts @@ -64,13 +64,6 @@ export function podInstall( } logger.info(stdout); if (stdout.includes('Pod installation complete')) { - // Remove build folder after pod install - if (buildFolder) { - buildFolder = join(iosDirectory, buildFolder); - if (existsSync(buildFolder)) { - rmdirSync(buildFolder, { recursive: true }); - } - } resolve(); } else { reject(new Error(podInstallErrorMessage)); diff --git a/packages/react-native/src/utils/versions.ts b/packages/react-native/src/utils/versions.ts index 1fc9c4a5f0e990..e2900881ed0e3a 100644 --- a/packages/react-native/src/utils/versions.ts +++ b/packages/react-native/src/utils/versions.ts @@ -25,3 +25,6 @@ export const reactNativeSvgTransformerVersion = '1.0.0'; export const reactNativeSvgVersion = '13.9.0'; export const babelRuntimeVersion = '7.21.0'; + +export const reactNativeAsyncStorageVersion = '1.18.1'; +export const reactNativeSafeAreaContextVersion = '4.5.1'; diff --git a/packages/storybook/migrations.json b/packages/storybook/migrations.json index 2217a2364e4f64..c09310446d83cc 100644 --- a/packages/storybook/migrations.json +++ b/packages/storybook/migrations.json @@ -56,6 +56,31 @@ } }, "packageJsonUpdates": { + "16.0.1": { + "version": "16.0.1-beta.0", + "packages": { + "@storybook/react-native": { + "version": "^6.5.3", + "alwaysAddToPackageJson": false + }, + "@storybook/addon-ondevice-actions": { + "version": "^6.5.3", + "alwaysAddToPackageJson": false + }, + "@storybook/addon-ondevice-backgrounds": { + "version": "^6.5.3", + "alwaysAddToPackageJson": false + }, + "@storybook/addon-ondevice-controls": { + "version": "^6.5.3", + "alwaysAddToPackageJson": false + }, + "@storybook/addon-ondevice-notes": { + "version": "^6.5.3", + "alwaysAddToPackageJson": false + } + } + }, "16.0.0": { "version": "16.0.0-beta.1", "packages": { diff --git a/packages/storybook/src/generators/init/init.ts b/packages/storybook/src/generators/init/init.ts index 1c6119c1dff850..95d2bff410ef1e 100644 --- a/packages/storybook/src/generators/init/init.ts +++ b/packages/storybook/src/generators/init/init.ts @@ -117,8 +117,6 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) { storybookReactNativeVersion; devDependencies['@storybook/addon-ondevice-notes'] = storybookReactNativeVersion; - devDependencies['react-native-storybook-loader'] = - reactNativeStorybookLoader; } } else { // TODO(katerina): Remove when Storybook v7 @@ -176,8 +174,6 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) { storybookReactNativeVersion; devDependencies['@storybook/addon-ondevice-notes'] = storybookReactNativeVersion; - devDependencies['react-native-storybook-loader'] = - reactNativeStorybookLoader; } } diff --git a/packages/storybook/src/utils/versions.ts b/packages/storybook/src/utils/versions.ts index fee5f276d5e6cc..e19d705d6481eb 100644 --- a/packages/storybook/src/utils/versions.ts +++ b/packages/storybook/src/utils/versions.ts @@ -7,7 +7,7 @@ export const svgrVersion = '^6.1.2'; export const urlLoaderVersion = '^4.1.1'; export const webpack5Version = '^5.64.0'; export const viteBuilderVersion = '^0.2.6'; -export const storybookReactNativeVersion = '^6.0.1-beta.11'; +export const storybookReactNativeVersion = '^6.5.3'; export const reactNativeStorybookLoader = '^2.0.5'; export const storybookSwcAddonVersion = '^1.1.7'; export const storybookNextAddonVersion = '^1.6.6';