Merge pull request #20719 from storybookjs/feat/react-native-v6-cli-init

CLI: Update init script for react-native v6.5
This commit is contained in:
Michael Shilman 2023-02-13 23:49:39 +08:00 committed by GitHub
commit b814979dd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 77 additions and 255 deletions

View File

@ -14,6 +14,7 @@ module.exports = {
'import/no-extraneous-dependencies': ignore,
'global-require': ignore,
'no-redeclare': ignore,
'react/prop-types': ignore,
},
},
{

View File

@ -1,54 +1,34 @@
import { join } from 'path';
import chalk from 'chalk';
import shell from 'shelljs';
import { getBabelDependencies, paddedLog, copyTemplate } from '../../helpers';
import { getCliDir } from '../../dirs';
import { copyTemplateFiles, getBabelDependencies } from '../../helpers';
import type { JsPackageManager } from '../../js-package-manager';
import type { NpmOptions } from '../../NpmOptions';
import { SupportedLanguage } from '../../project_types';
const generator = async (
packageManager: JsPackageManager,
npmOptions: NpmOptions,
installServer: boolean
npmOptions: NpmOptions
): Promise<void> => {
// set correct project name on entry files if possible
const dirname = shell.ls('-d', 'ios/*.xcodeproj').stdout;
// Only notify about app name if running in React Native vanilla (Expo projects do not have ios directory)
if (dirname) {
const projectName = dirname.slice('ios/'.length, dirname.length - '.xcodeproj'.length - 1);
if (projectName) {
shell.sed('-i', '%APP_NAME%', projectName, 'storybook/index.js');
} else {
paddedLog(
chalk.red(
'ERR: Could not determine project name, to fix: https://github.com/storybookjs/storybook/issues/1277'
)
);
}
}
const packageJson = packageManager.retrievePackageJson();
const missingReactDom =
!packageJson.dependencies['react-dom'] && !packageJson.devDependencies['react-dom'];
const reactVersion = packageJson.dependencies.react;
// should resolve to latest 5.3 version, this is required until react-native storybook supports v6
const webAddonsV5 = [
'@storybook/addon-links@^5.3',
'@storybook/addon-knobs@^5.3',
'@storybook/addon-actions@^5.3',
const packagesToResolve = [
// addon-ondevice-controls peer deps
'react-native-safe-area-context',
'@react-native-async-storage/async-storage',
'@react-native-community/datetimepicker',
'@react-native-community/slider',
];
const nativeAddons = ['@storybook/addon-ondevice-knobs', '@storybook/addon-ondevice-actions'];
const packagesToResolve = [
...nativeAddons,
'@storybook/react-native',
installServer && '@storybook/react-native-server',
].filter(Boolean);
// change these to latest version once v6 stable is released
const packagesWithFixedVersion = [
'@storybook/addon-actions@^6.5.14',
'@storybook/addon-controls@^6.5.14',
'@storybook/addon-ondevice-controls@6.5.0-rc.0',
'@storybook/addon-ondevice-actions@6.5.0-rc.0',
'@storybook/react-native@6.5.0-rc.0',
];
const resolvedPackages = await packageManager.getVersionedPackages(packagesToResolve);
@ -56,21 +36,25 @@ const generator = async (
const packages = [
...babelDependencies,
...packagesWithFixedVersion,
...resolvedPackages,
...webAddonsV5,
missingReactDom && reactVersion && `react-dom@${reactVersion}`,
].filter(Boolean);
packageManager.addDependencies({ ...npmOptions, packageJson }, packages);
packageManager.addScripts({
'storybook-generate': 'sb-rn-get-stories',
'storybook-watch': 'sb-rn-watcher',
});
if (installServer) {
packageManager.addStorybookCommandInScripts({
port: 7007,
});
}
const storybookConfigFolder = '.storybook';
const templateDir = join(getCliDir(), 'templates', 'react-native');
copyTemplate(templateDir);
await copyTemplateFiles({
renderer: 'react-native',
language: SupportedLanguage.JAVASCRIPT,
destination: storybookConfigFolder,
includeCommonAssets: false,
});
};
export default generator;

View File

@ -4,7 +4,7 @@ import { dedent } from 'ts-dedent';
import type { NpmOptions } from '../NpmOptions';
import type { SupportedRenderers, SupportedFrameworks, Builder } from '../project_types';
import { externalFrameworks, CoreBuilder } from '../project_types';
import { getBabelDependencies, copyComponents } from '../helpers';
import { getBabelDependencies, copyTemplateFiles } from '../helpers';
import { configureMain, configurePreview } from './configure';
import type { JsPackageManager } from '../js-package-manager';
import { getPackageDetails } from '../js-package-manager';
@ -309,6 +309,10 @@ export async function baseGenerator(
if (addComponents) {
const templateLocation = hasFrameworkTemplates(framework) ? framework : rendererId;
await copyComponents(templateLocation, language, componentsDestinationPath);
await copyTemplateFiles({
renderer: templateLocation,
language,
destination: componentsDestinationPath,
});
}
}

View File

@ -76,7 +76,7 @@ describe('Helpers', () => {
(filePath) =>
componentsDirectory.includes(filePath) || filePath === '@storybook/react/template/cli'
);
await helpers.copyComponents('react', language);
await helpers.copyTemplateFiles({ renderer: 'react', language });
const copySpy = jest.spyOn(fse, 'copy');
expect(copySpy).toHaveBeenNthCalledWith(
@ -95,7 +95,7 @@ describe('Helpers', () => {
(fse.pathExists as jest.Mock).mockImplementation((filePath) => {
return filePath === '@storybook/react/template/cli' || filePath === './src';
});
await helpers.copyComponents('react', SupportedLanguage.JAVASCRIPT);
await helpers.copyTemplateFiles({ renderer: 'react', language: SupportedLanguage.JAVASCRIPT });
expect(fse.copy).toHaveBeenCalledWith(expect.anything(), './src/stories', expect.anything());
});
@ -103,7 +103,7 @@ describe('Helpers', () => {
(fse.pathExists as jest.Mock).mockImplementation((filePath) => {
return filePath === '@storybook/react/template/cli';
});
await helpers.copyComponents('react', SupportedLanguage.JAVASCRIPT);
await helpers.copyTemplateFiles({ renderer: 'react', language: SupportedLanguage.JAVASCRIPT });
expect(fse.copy).toHaveBeenCalledWith(expect.anything(), './stories', expect.anything());
});
@ -111,7 +111,7 @@ describe('Helpers', () => {
const renderer = 'unknown renderer' as SupportedRenderers;
const expectedMessage = `Unsupported renderer: ${renderer}`;
await expect(
helpers.copyComponents(renderer, SupportedLanguage.JAVASCRIPT)
helpers.copyTemplateFiles({ renderer, language: SupportedLanguage.JAVASCRIPT })
).rejects.toThrowError(expectedMessage);
});

View File

@ -186,17 +186,25 @@ export function copyTemplate(templateRoot: string, destination = '.') {
fse.copySync(templateDir, destination, { overwrite: true });
}
export async function copyComponents(
renderer: SupportedFrameworks | SupportedRenderers,
language: SupportedLanguage,
destination?: string
) {
type CopyTemplateFilesOptions = {
renderer: SupportedFrameworks | SupportedRenderers;
language: SupportedLanguage;
includeCommonAssets?: boolean;
destination?: string;
};
export async function copyTemplateFiles({
renderer,
language,
destination,
includeCommonAssets = true,
}: CopyTemplateFilesOptions) {
const languageFolderMapping: Record<SupportedLanguage, string> = {
[SupportedLanguage.JAVASCRIPT]: 'js',
[SupportedLanguage.TYPESCRIPT]: 'ts',
[SupportedLanguage.TYPESCRIPT_LEGACY]: 'ts-legacy',
};
const componentsPath = async () => {
const templatePath = async () => {
const baseDir = getRendererDir(renderer);
const assetsDir = join(baseDir, 'template/cli');
@ -234,10 +242,12 @@ export async function copyComponents(
};
const destinationPath = destination ?? (await targetPath());
await fse.copy(join(getCliDir(), 'rendererAssets/common'), destinationPath, {
overwrite: true,
});
await fse.copy(await componentsPath(), destinationPath, { overwrite: true });
if (includeCommonAssets) {
await fse.copy(join(getCliDir(), 'rendererAssets/common'), destinationPath, {
overwrite: true,
});
}
await fse.copy(await templatePath(), destinationPath, { overwrite: true });
}
// Given a package.json, finds any official storybook package within it

View File

@ -78,21 +78,9 @@ const installStorybook = <Project extends ProjectType>(
);
case ProjectType.REACT_NATIVE: {
return (
options.yes
? Promise.resolve({ server: true })
: (prompts([
{
type: 'confirm',
name: 'server',
message:
'Do you want to install dependencies necessary to run Storybook server? You can manually do it later by install @storybook/react-native-server',
initial: false,
},
]) as Promise<{ server: boolean }>)
)
.then(({ server }) => reactNativeGenerator(packageManager, npmOptions, server))
.then(commandLog('Adding Storybook support to your "React Native" app\n'));
return reactNativeGenerator(packageManager, npmOptions).then(
commandLog('Adding Storybook support to your "React Native" app\n')
);
}
case ProjectType.QWIK: {
@ -349,28 +337,28 @@ async function doInitiate(options: CommandOptions, pkg: PackageJson): Promise<vo
telemetry('init', { projectType });
}
await automigrate({ yes: options.yes || process.env.CI === 'true', packageManager: pkgMgr });
logger.log('\nTo run your Storybook, type:\n');
if (projectType === ProjectType.ANGULAR) {
codeLog([`ng run ${installResult.projectName}:storybook`]);
} else {
codeLog([packageManager.getRunStorybookCommand()]);
if (projectType !== ProjectType.REACT_NATIVE) {
await automigrate({ yes: options.yes || process.env.CI === 'true', packageManager: pkgMgr });
}
logger.log('\nFor more information visit:', chalk.cyan('https://storybook.js.org'));
if (projectType === ProjectType.REACT_NATIVE) {
const REACT_NATIVE_REPO = 'https://github.com/storybookjs/react-native';
if (projectType === ProjectType.ANGULAR) {
logger.log('\nTo run your Storybook, type:\n');
codeLog([`ng run ${installResult.projectName}:storybook`]);
} else if (projectType === ProjectType.REACT_NATIVE) {
logger.log();
logger.log(chalk.red('NOTE: installation is not 100% automated.'));
logger.log(chalk.yellow('NOTE: installation is not 100% automated.\n'));
logger.log(`To quickly run Storybook, replace contents of your app entry with:\n`);
codeLog(["export {default} from './storybook';"]);
codeLog(["export {default} from './.storybook';"]);
logger.log('\n Then to run your Storybook, type:\n');
codeLog([packageManager.getRunCommand('start')]);
logger.log('\n For more in information, see the github readme:\n');
logger.log(chalk.cyan(REACT_NATIVE_REPO));
logger.log(chalk.cyan('https://github.com/storybookjs/react-native'));
logger.log();
} else {
logger.log('\nTo run your Storybook, type:\n');
codeLog([packageManager.getRunStorybookCommand()]);
}
// Add a new line for the clear visibility.

View File

@ -1,3 +0,0 @@
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
import '@storybook/addon-knobs/register';

View File

@ -1,29 +0,0 @@
// if you use expo remove this line
import { AppRegistry } from 'react-native';
import { getStorybookUI, configure, addDecorator } from '@storybook/react-native';
import { withKnobs } from '@storybook/addon-knobs';
import './rn-addons';
// enables knobs for all stories
addDecorator(withKnobs);
// import stories
configure(
() => {
require('./stories');
},
module,
false
);
// Refer to https://github.com/storybookjs/react-native/tree/master/app/react-native#getstorybookui-options
// To find allowed options for getStorybookUI
const StorybookUIRoot = getStorybookUI({});
// If you are using React Native vanilla and after installation you don't see your app name here, write it manually.
// If you use Expo you should remove this line.
AppRegistry.registerComponent('%APP_NAME%', () => StorybookUIRoot);
export default StorybookUIRoot;

View File

@ -1,2 +0,0 @@
import '@storybook/addon-ondevice-actions/register';
import '@storybook/addon-ondevice-knobs/register';

View File

@ -1,20 +0,0 @@
import { action } from '@storybook/addon-actions';
import { text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react-native';
import React from 'react';
import { Text } from 'react-native';
import Button from '.';
import CenterView from '../CenterView';
storiesOf('Button', module)
.addDecorator((getStory) => <CenterView>{getStory()}</CenterView>)
.add('with text', () => (
<Button onPress={action('clicked-text')}>
<Text>{text('Button text', 'Hello Button')}</Text>
</Button>
))
.add('with some emoji', () => (
<Button onPress={action('clicked-emoji')}>
<Text>😀 😎 👍 💯</Text>
</Button>
));

View File

@ -1,17 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TouchableHighlight } from 'react-native';
export default function Button({ onPress, label }) {
return <TouchableHighlight onPress={onPress}>{label}</TouchableHighlight>;
}
Button.defaultProps = {
label: null,
onPress: () => {},
};
Button.propTypes = {
label: PropTypes.node,
onPress: PropTypes.func,
};

View File

@ -1,16 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import style from './style';
export default function CenterView({ label }) {
return <View style={style.main}>{label}</View>;
}
CenterView.defaultProps = {
label: null,
};
CenterView.propTypes = {
label: PropTypes.node,
};

View File

@ -1,8 +0,0 @@
export default {
main: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
};

View File

@ -1,6 +0,0 @@
import React from 'react';
import { linkTo } from '@storybook/addon-links';
import { storiesOf } from '@storybook/react-native';
import Welcome from '.';
storiesOf('Welcome', module).add('to Storybook', () => <Welcome showApp={linkTo('Button')} />);

View File

@ -1,57 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
export default class Welcome extends React.Component {
styles = {
wrapper: {
flex: 1,
padding: 24,
justifyContent: 'center',
},
header: {
fontSize: 18,
marginBottom: 18,
},
content: {
fontSize: 12,
marginBottom: 10,
lineHeight: 18,
},
};
showApp = (event) => {
const { showApp } = this.props;
event.preventDefault();
if (showApp) {
showApp();
}
};
render() {
return (
<View style={this.styles.wrapper}>
<Text style={this.styles.header}>Welcome to React Native Storybook</Text>
<Text style={this.styles.content}>
This is a UI Component development environment for your React Native app. Here you can
display and interact with your UI components as stories. A story is a single state of one
or more UI components. You can have as many stories as you want. In other words a story is
like a visual test case.
</Text>
<Text style={this.styles.content}>
We have added some stories inside the "storybook/stories" directory for examples. Try
editing the "storybook/stories/Welcome.js" file to edit this message.
</Text>
</View>
);
}
}
Welcome.defaultProps = {
showApp: null,
};
Welcome.propTypes = {
showApp: PropTypes.func,
};

View File

@ -1,2 +0,0 @@
import './Button/Button.stories';
import './Welcome/Welcome.stories';

View File

@ -1,5 +0,0 @@
export const parameters = {
server: {
url: 'http://storybook-server-demo.netlify.app/api',
},
};