Skip to content

Commit

Permalink
feat: setup Storybook 8 (#405)
Browse files Browse the repository at this point in the history
* feat: initial Storybook setup

* refactor: migrate existing sandbox to storybook

* chore: remove last mentions of the old sandbox

* chore: clean unused package

* chore: add example env values for storybook
  • Loading branch information
tsyirvo authored Nov 26, 2024
1 parent 821027f commit 7412ab7
Show file tree
Hide file tree
Showing 62 changed files with 1,703 additions and 810 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ EAS_PROJECT_ID=""
EXPO_APPLE_TEAM_ID=""

API_URL=""
STORYBOOK_ENABLED=""
ITUNES_ITEM_ID=""

SENTRY_DSN=""
Expand Down
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ EAS_PROJECT_ID=""
EXPO_APPLE_TEAM_ID=""

API_URL=""
STORYBOOK_ENABLED=""
ITUNES_ITEM_ID=""

SENTRY_DSN=""
Expand Down
1 change: 1 addition & 0 deletions .env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ EAS_PROJECT_ID=""
EXPO_APPLE_TEAM_ID=""

API_URL=""
STORYBOOK_ENABLED=""
ITUNES_ITEM_ID=""

SENTRY_DSN=""
Expand Down
11 changes: 11 additions & 0 deletions .storybook/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { view } from './storybook.requires';

const StorybookUIRoot = view.getStorybookUI({
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
});

export default StorybookUIRoot;
12 changes: 12 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { StorybookConfig } from '@storybook/react-native';

const main: StorybookConfig = {
stories: ['../src/**/*.stories.?(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
'@storybook/addon-ondevice-actions',
],
};

export default main;
25 changes: 25 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
import type { Preview } from '@storybook/react';

const preview: Preview = {
decorators: [withBackgrounds],

parameters: {
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#1E1E1E' },
{ name: 'light', value: '#FFFFFF' },
],
},
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};

export default preview;
50 changes: 50 additions & 0 deletions .storybook/storybook.requires.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* do not change this file, it is auto generated by storybook. */

import { start, updateView } from "@storybook/react-native";

import "@storybook/addon-ondevice-controls/register";
import "@storybook/addon-ondevice-backgrounds/register";
import "@storybook/addon-ondevice-actions/register";

const normalizedStories = [
{
titlePrefix: "",
directory: "./src",
files: "**/*.stories.?(ts|tsx|js|jsx)",
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
// @ts-ignore
req: require.context(
"../src",
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
];

declare global {
var view: ReturnType<typeof start>;
var STORIES: typeof normalizedStories;
}

const annotations = [
require("./preview"),
require("@storybook/react-native/preview"),
require("@storybook/addon-ondevice-actions/preview"),
];

global.STORIES = normalizedStories;

// @ts-ignore
module?.hot?.accept?.();

if (!global.view) {
global.view = start({
annotations,
storyEntries: normalizedStories,
});
} else {
updateView(global.view, annotations, normalizedStories);
}

export const view = global.view;
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
"doppler.autocomplete.enable": true,
"doppler.hover.enable": true,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
"typescript.enablePromptUseWorkspaceTsdk": true,
"react-native-storybooks.storyRegex": "**/*.stories.?(ts|tsx|js|jsx)"
}
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- [Environments](#environments)
- [Internationalization](#internationalization)
- [Adding images](#adding-images)
- [Using the custom Sandbox](#using-the-custom-sandbox)
- [Using Storybook](#using-storybook)
- [Tests](#tests)
- [Formatting and type checking](#formatting-and-type-checking)
- [Github Actions](#github-actions)
Expand All @@ -24,7 +24,7 @@ It's a basic start, but with most of the common dependencies and tools I use so

Check the [React Native docs](https://reactnative.dev/docs/environment-setup) on how to properly setup your dev environment. It uses Expo with a custom Development Build, so you also need to setup [Expo tooling](https://docs.expo.dev/).

On the Developer Experience side, a test stack is setup (unit, functional and E2E), a CI on _Github Actions_, a custom _Storybook_, _TypeScript_ is also configured with _ESLint_ and _Prettier_. Commits are linted to automate the release workflows and the changelog generation.
On the Developer Experience side, a test stack is setup (unit, functional and E2E), a CI on _Github Actions_, _Storybook_, _TypeScript_ is also configured with _ESLint_ and _Prettier_. Commits are linted to automate the release workflows and the changelog generation.

There are also some utilities like:

Expand Down Expand Up @@ -140,11 +140,15 @@ To simplify adding new images to the project and optimizing them, you can run th
yarn image:add [path/to/the/image/to/add|path/to/the/folder]
```

## Using the custom Sandbox
## Using Storybook

A custom _Sandbox_ is configured with some basic examples and navigation. Once [StoryBook](https://storybook.js.org/) for React Native will support V8, I'll migrate to using that.
A _Storybook_ is configured with some basic stories.

To access it, you can access the dev menu on the device and select _Toggle Sandbox_ to have it shown in place of the app.
To access it, you simply have to run the app with the following command:

```
yarn start:storybook
```

## Tests

Expand Down
4 changes: 3 additions & 1 deletion env.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const client = z.object({
VERSION: z.string(),

// ADD CLIENT ENV VARS HERE
STORYBOOK_ENABLED: z.string().optional(),
API_URL: z.string(),
ITUNES_ITEM_ID: z.string(),
FLAGSMITH_KEY: z.string(),
Expand Down Expand Up @@ -63,13 +64,14 @@ const buildTime = z.object({

const _clientEnv = {
APP_ENV,
APP_NAME: process.env.APP_NAME,
BUNDLE_ID: withEnvSuffix(BUNDLE_ID),
PACKAGE: withEnvSuffix(PACKAGE),
VERSION: packageJSON.version,

// ADD ENV VARS HERE TOO
APP_NAME: process.env.APP_NAME,
API_URL: process.env.API_URL,
STORYBOOK_ENABLED: process.env.STORYBOOK_ENABLED,
ITUNES_ITEM_ID: process.env.ITUNES_ITEM_ID,
FLAGSMITH_KEY: process.env.FLAGSMITH_KEY,
AMPLITUDE_API_KEY: process.env.AMPLITUDE_API_KEY,
Expand Down
12 changes: 11 additions & 1 deletion metro.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
// Learn more https://docs.expo.io/guides/customizing-metro
// This replaces `const { getDefaultConfig } = require('expo/metro-config');`
const path = require('path');

const { getSentryExpoConfig } = require('@sentry/react-native/metro');
const withStorybook = require('@storybook/react-native/metro/withStorybook');

// This replaces `const config = getDefaultConfig(__dirname);`
const config = getSentryExpoConfig(__dirname);

module.exports = config;
module.exports = withStorybook(config, {
enabled: process.env.STORYBOOK_ENABLED === 'true',
onDisabledRemoveStorybook: true,
websockets: {
port: 7007,
host: 'localhost',
},
});
43 changes: 27 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,33 @@
"node": ">=20"
},
"scripts": {
"start:dev": "cross-env EXPO_NO_DOTENV=1 expo start --dev-client",
"start:staging": "cross-env APP_ENV=staging expo start --no-dev --minify",
"start:production": "cross-env APP_ENV=production expo start --no-dev --minify",
"start:dev": "EXPO_NO_DOTENV=1 expo start --dev-client",
"start:staging": "APP_ENV=staging expo start --no-dev --minify",
"start:production": "APP_ENV=production expo start --no-dev --minify",
"//// BUILDS ////": "",
"build:dev:ios": "cross-env EXPO_NO_DOTENV=1 eas build --profile development --platform ios --local",
"build:dev:android": "cross-env EXPO_NO_DOTENV=1 eas build --profile development --platform android --local",
"build:dev-device:ios": "cross-env EXPO_NO_DOTENV=1 eas build --profile development-device --platform ios --local",
"build:staging:ios": "cross-env APP_ENV=staging eas build --profile staging --platform ios --local",
"build:staging:android": "cross-env APP_ENV=staging eas build --profile staging --platform android --local",
"build:production:ios": "cross-env APP_ENV=production eas build --profile production --platform ios --local",
"build:production:android": "cross-env APP_ENV=production eas build --profile production --platform android --local",
"build:dev:ios": "EXPO_NO_DOTENV=1 eas build --profile development --platform ios --local",
"build:dev:android": "EXPO_NO_DOTENV=1 eas build --profile development --platform android --local",
"build:dev-device:ios": "EXPO_NO_DOTENV=1 eas build --profile development-device --platform ios --local",
"build:staging:ios": "APP_ENV=staging eas build --profile staging --platform ios --local",
"build:staging:android": "APP_ENV=staging eas build --profile staging --platform android --local",
"build:production:ios": "APP_ENV=production eas build --profile production --platform ios --local",
"build:production:android": "APP_ENV=production eas build --profile production --platform android --local",
"//// TESTS ////": "",
"test": "jest --coverage=false",
"test:coverage": "jest",
"test:e2e": "maestro test ./src/e2e",
"//// QUALITY ////": "",
"lint:ts": "yarn tsc",
"lint": "eslint ./ --ext .js,.ts,.tsx,.json --fix --ignore-path .gitignore",
"prettify": "yarn prettier --write './**/*.{js,ts,tsx}'",
"pretty:check": "yarn prettier --check './**/*.{js,ts,tsx}'",
"prettify": "yarn prettier --write './**/*.{js,ts,tsx}' '!.storybook/storybook.requires.ts'",
"pretty:check": "yarn prettier --check './**/*.{js,ts,tsx}' '!.storybook/storybook.requires.ts'",
"//// UTILITIES ////": "",
"codegen": "graphql-codegen --config codegen.ts",
"image:add": "ts-node -T ./scripts/importImages/index.ts",
"generate:icons": "svgr --config-file src/shared/icons/svgs/config/svgrConfig.js --svgo-config src/shared/icons/svgs/config/svgoConfig.js --index-template src/shared/icons/svgs/config/index-template.js --template src/shared/icons/svgs/config/icon-template.js --out-dir src/shared/icons/components -- src/shared/icons/svgs && eslint './src/shared/icons/components/**/*.{ts,tsx}' --ext .ts,.tsx --fix --quiet && prettier --write './src/shared/icons/components/**/*.{ts,tsx}'",
"//// STORYBOOK ////": "",
"storybook:generate": "sb-rn-get-stories",
"start:storybook": "STORYBOOK_ENABLED='true' expo start",
"//// VERSIONNING ////": "",
"version:bump": "standard-version --skip.commit --skip.changelog --skip.tag",
"version:tag": "standard-version",
Expand All @@ -59,11 +62,11 @@
},
"dependencies": {
"@amplitude/analytics-react-native": "1.4.10",
"@gorhom/bottom-sheet": "5.0.6",
"@hookform/resolvers": "3.9.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.1",
"@react-navigation/native": "6.1.17",
"@react-navigation/native-stack": "6.9.26",
"@sentry/react-native": "~5.24.3",
"@shopify/flash-list": "1.6.4",
"@shopify/restyle": "2.4.4",
Expand All @@ -83,7 +86,7 @@
"expo-linking": "~6.3.1",
"expo-localization": "~15.0.3",
"expo-network": "~6.0.1",
"expo-router": "~3.5.23",
"expo-router": "~3.5.24",
"expo-secure-store": "~13.0.2",
"expo-splash-screen": "~0.27.6",
"expo-status-bar": "~1.12.1",
Expand Down Expand Up @@ -132,6 +135,13 @@
"@graphql-codegen/typescript": "4.1.0",
"@graphql-codegen/typescript-operations": "4.3.0",
"@graphql-codegen/typescript-react-query": "6.1.0",
"@react-native-community/datetimepicker": "^8.2.0",
"@react-native-community/slider": "^4.5.5",
"@storybook/addon-actions": "8.4.2",
"@storybook/addon-ondevice-actions": "8.4.2",
"@storybook/addon-ondevice-backgrounds": "8.4.2",
"@storybook/addon-ondevice-controls": "8.4.2",
"@storybook/react-native": "^8.4.2",
"@svgr/cli": "8.1.0",
"@testing-library/jest-native": "5.4.3",
"@testing-library/react-native": "12.7.2",
Expand All @@ -147,9 +157,9 @@
"@types/semver": "7.5.8",
"@types/sharp": "0.31.0",
"app-icon-badge": "0.0.15",
"babel-loader": "^8.4.1",
"babel-plugin-root-import": "6.6.0",
"babel-plugin-transform-remove-console": "6.9.4",
"cross-env": "7.0.3",
"dotenv": "16.4.5",
"eslint": "8.57.1",
"eslint-config-tsyirvo-react-native": "https://github.com/tsyirvo/eslint-config-tsyirvo-react-native.git#develop",
Expand All @@ -167,6 +177,7 @@
"prettier": "3.2.5",
"sharp": "0.31.1",
"standard-version": "9.5.0",
"storybook": "8.4.2",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",
"typescript": "~5.3.3"
Expand Down Expand Up @@ -216,7 +227,7 @@
"!**/app.config.ts",
"!**/**.e2e.ts",
"!src/components/icons/components/**",
"!src/sandbox/**",
"!**/stories/**",
"!**/**.types.ts"
]
},
Expand Down
4 changes: 4 additions & 0 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import Toast from 'react-native-toast-message';

import { ErrorMonitoring } from '$core/monitoring';
import { useAppScreenTracking } from '$core/navigation/hooks/useAppScreenTracking';
import { useAppStateTracking } from '$core/navigation/hooks/useAppStateTracking';
import { Providers } from '$core/providers/Providers';
Expand All @@ -15,6 +16,9 @@ import 'react-native-gesture-handler';

import '../core/i18n';

// Sentry is initialized here so that it runs before Sentry.wrap()
ErrorMonitoring.init();

const RootLayout = () => {
useCheckNetworkStateOnMount();
useAppStateTracking();
Expand Down
14 changes: 13 additions & 1 deletion src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Stack, useRouter } from 'expo-router';
import { useTranslation } from 'react-i18next';

import { config } from '$core/constants';
import { Header, Informations, Version } from '$features/home/components';
import { Button } from '$shared/uiKit/button';
import { Box, Text } from '$shared/uiKit/primitives';
Expand Down Expand Up @@ -46,4 +47,15 @@ const HomeScreen = () => {
);
};

export default HomeScreen;
// eslint-disable-next-line import/no-mutable-exports
let EntryPoint = HomeScreen;

if (config.isStorybookEnabled) {
// eslint-disable-next-line
const StorybookUI = require('../../.storybook').default;

// eslint-disable-next-line
EntryPoint = () => <StorybookUI />;
}

export default EntryPoint;
2 changes: 0 additions & 2 deletions src/core/bootstrapExternalSdks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Analytics } from './analytics';
import { Attribution } from './attribution';
import { initDateLocale } from './date';
import { getSupportedLocale } from './i18n';
import { ErrorMonitoring } from './monitoring';
import { Notifications } from './notifications';
import { Purchase } from './purchase';

Expand All @@ -17,7 +16,6 @@ const initDateLib = (locale: string) => {

export const bootstrapExternalSdks = async () => {
// Core services to init first in a specific order
ErrorMonitoring.init();
await Analytics.init();
await Attribution.init();

Expand Down
2 changes: 2 additions & 0 deletions src/core/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const runtimeVersion = Constants.expoConfig?.runtimeVersion;
const iosBundleIdentifier = Constants.expoConfig?.ios?.bundleIdentifier ?? '';
const androidPackageName = Constants.expoConfig?.android?.package ?? '';
const apiURL = Env.API_URL;
const isStorybookEnabled = Env.STORYBOOK_ENABLED === 'true';
const sentryDsn = Env.SENTRY_DSN;
const amplitudeApiKey = Env.AMPLITUDE_API_KEY;
const flagsmithKey = Env.FLAGSMITH_KEY;
Expand All @@ -37,6 +38,7 @@ export const config = {
runtimeVersion,
bundleId: IS_IOS ? iosBundleIdentifier : androidPackageName,
apiURL,
isStorybookEnabled,
// SDKs
sentryDsn,
amplitudeApiKey,
Expand Down
Loading

0 comments on commit 7412ab7

Please sign in to comment.