diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e7b5f1f1e07..d89b0e260576 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -980,22 +980,22 @@ workflows: requires: - build - create-sandboxes: - parallelism: 37 + parallelism: 38 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 37 + parallelism: 38 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 34 + parallelism: 35 requires: - build-sandboxes - e2e-production: - parallelism: 32 + parallelism: 33 requires: - build-sandboxes - e2e-dev: @@ -1003,7 +1003,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 32 + parallelism: 33 requires: - build-sandboxes - vitest-integration: diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 0b09a2a6f710..c470d2578b98 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,8 @@ +## 8.5.0-alpha.6 + +- CLI: Fix qwik init - [#29632](https://github.com/storybookjs/storybook/pull/29632), thanks @shilman! +- React Native Web: Add framework, CLI integration, sandboxes - [#29520](https://github.com/storybookjs/storybook/pull/29520), thanks @shilman! + ## 8.5.0-alpha.5 - Addon Test: Only optimize react deps if applicable in vitest-plugin - [#29617](https://github.com/storybookjs/storybook/pull/29617), thanks @yannbf! diff --git a/code/core/src/cli/detect.ts b/code/core/src/cli/detect.ts index 09ef8cae02cf..5b50abee4301 100644 --- a/code/core/src/cli/detect.ts +++ b/code/core/src/cli/detect.ts @@ -130,6 +130,8 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp // Fallback to Vite or Webpack based on project type switch (projectType) { + case ProjectType.REACT_NATIVE_WEB: + return CoreBuilder.Vite; case ProjectType.REACT_SCRIPTS: case ProjectType.ANGULAR: case ProjectType.REACT_NATIVE: // technically react native doesn't use webpack, we just want to set something diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 47df29aba89e..27e91f001ac1 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -157,6 +157,7 @@ export const frameworkToDefaultBuilder: Record< 'preact-vite': CoreBuilder.Vite, 'preact-webpack5': CoreBuilder.Webpack5, qwik: CoreBuilder.Vite, + 'react-native-web-vite': CoreBuilder.Vite, 'react-vite': CoreBuilder.Vite, 'react-webpack5': CoreBuilder.Webpack5, 'server-webpack5': CoreBuilder.Webpack5, @@ -193,6 +194,13 @@ export async function getVersionSafe(packageManager: JsPackageManager, packageNa return undefined; } +export const cliStoriesTargetPath = async () => { + if (existsSync('./src')) { + return './src/stories'; + } + return './stories'; +}; + export async function copyTemplateFiles({ packageManager, renderer, @@ -252,14 +260,7 @@ export async function copyTemplateFiles({ throw new Error(`Unsupported renderer: ${renderer} (${baseDir})`); }; - const targetPath = async () => { - if (existsSync('./src')) { - return './src/stories'; - } - return './stories'; - }; - - const destinationPath = destination ?? (await targetPath()); + const destinationPath = destination ?? (await cliStoriesTargetPath()); if (commonAssetsDir) { await cp(commonAssetsDir, destinationPath, { recursive: true, diff --git a/code/core/src/cli/project_types.ts b/code/core/src/cli/project_types.ts index 5d7d4a4d3ead..25148d2bc089 100644 --- a/code/core/src/cli/project_types.ts +++ b/code/core/src/cli/project_types.ts @@ -47,6 +47,7 @@ export enum ProjectType { REACT = 'REACT', REACT_SCRIPTS = 'REACT_SCRIPTS', REACT_NATIVE = 'REACT_NATIVE', + REACT_NATIVE_WEB = 'REACT_NATIVE_WEB', REACT_PROJECT = 'REACT_PROJECT', WEBPACK_REACT = 'WEBPACK_REACT', NEXTJS = 'NEXTJS', diff --git a/code/core/src/common/utils/framework-to-renderer.ts b/code/core/src/common/utils/framework-to-renderer.ts index 1dd1fa0b811e..7ae4c3b057a5 100644 --- a/code/core/src/common/utils/framework-to-renderer.ts +++ b/code/core/src/common/utils/framework-to-renderer.ts @@ -32,6 +32,7 @@ export const frameworkToRenderer: Record< html: 'html', preact: 'preact', 'react-native': 'react-native', + 'react-native-web-vite': 'react', react: 'react', server: 'server', svelte: 'svelte', diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index 0d8645592db3..493d40295bc9 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -48,6 +48,7 @@ export default { '@storybook/nextjs': '8.5.0-alpha.5', '@storybook/preact-vite': '8.5.0-alpha.5', '@storybook/preact-webpack5': '8.5.0-alpha.5', + '@storybook/react-native-web-vite': '8.5.0-alpha.5', '@storybook/react-vite': '8.5.0-alpha.5', '@storybook/react-webpack5': '8.5.0-alpha.5', '@storybook/server-webpack5': '8.5.0-alpha.5', diff --git a/code/core/src/types/modules/frameworks.ts b/code/core/src/types/modules/frameworks.ts index 2f2028db810b..e3e1b6383a7f 100644 --- a/code/core/src/types/modules/frameworks.ts +++ b/code/core/src/types/modules/frameworks.ts @@ -8,6 +8,7 @@ export type SupportedFrameworks = | 'nextjs' | 'preact-vite' | 'preact-webpack5' + | 'react-native-web-vite' | 'react-vite' | 'react-webpack5' | 'server-webpack5' diff --git a/code/e2e-tests/addon-actions.spec.ts b/code/e2e-tests/addon-actions.spec.ts index 861a392070b7..f049cbe3060a 100644 --- a/code/e2e-tests/addon-actions.spec.ts +++ b/code/e2e-tests/addon-actions.spec.ts @@ -12,6 +12,10 @@ test.describe('addon-actions', () => { templateName.includes('svelte') && templateName.includes('prerelease'), 'Svelte 5 prerelase does not support automatic actions with our current example components yet' ); + test.skip( + templateName.includes('react-native-web'), + 'React Native uses onPress rather than onClick' + ); await page.goto(storybookUrl); const sbPage = new SbPage(page, expect); sbPage.waitUntilLoaded(); diff --git a/code/e2e-tests/addon-controls.spec.ts b/code/e2e-tests/addon-controls.spec.ts index 23909fd707d0..6ba84552b81e 100644 --- a/code/e2e-tests/addon-controls.spec.ts +++ b/code/e2e-tests/addon-controls.spec.ts @@ -4,9 +4,12 @@ import process from 'process'; import { SbPage } from './util'; const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; +const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; test.describe('addon-controls', () => { test('should change component when changing controls', async ({ page }) => { + test.skip(templateName.includes('react-native-web'), 'React Native CSS behaves differently'); + await page.goto(storybookUrl); const sbPage = new SbPage(page, expect); await sbPage.waitUntilLoaded(); diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index 99b22d1d2e85..54c046a8aec7 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -123,6 +123,7 @@ test.describe('addon-docs', () => { // - template: https://638db567ed97c3fb3e21cc22-ulhjwkqzzj.chromatic.com/?path=/docs/addons-docs-docspage-basic--docs // - real: https://638db567ed97c3fb3e21cc22-ulhjwkqzzj.chromatic.com/?path=/docs/example-button--docs 'lit-vite', + 'react-native-web', ]; test.skip( new RegExp(`^${skipped.join('|')}`, 'i').test(`${templateName}`), diff --git a/code/e2e-tests/addon-interactions.spec.ts b/code/e2e-tests/addon-interactions.spec.ts index b5a703e2d1e8..b1cb9ea9884a 100644 --- a/code/e2e-tests/addon-interactions.spec.ts +++ b/code/e2e-tests/addon-interactions.spec.ts @@ -23,6 +23,10 @@ test.describe('addon-interactions', () => { /^(lit)/i.test(`${templateName}`), `Skipping ${templateName}, which does not support addon-interactions` ); + test.skip( + templateName.includes('react-native-web'), + 'React Native does not use className locators' + ); const sbPage = new SbPage(page, expect); diff --git a/code/frameworks/react-native-web-vite/README.md b/code/frameworks/react-native-web-vite/README.md new file mode 100644 index 000000000000..5bdae8f819c0 --- /dev/null +++ b/code/frameworks/react-native-web-vite/README.md @@ -0,0 +1,3 @@ +# Storybook for React Native Web & Vite + +See [documentation](https://storybook.js.org/docs/get-started/frameworks/react-native-web-vite?renderer=react-native-web) for installation instructions, usage examples, APIs, and more. diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json new file mode 100644 index 000000000000..8fb49dd2eb64 --- /dev/null +++ b/code/frameworks/react-native-web-vite/package.json @@ -0,0 +1,83 @@ +{ + "name": "@storybook/react-native-web-vite", + "version": "8.5.0-alpha.5", + "description": "Develop react-native components an isolated web environment with hot reloading.", + "keywords": [ + "storybook" + ], + "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/react-native-web-vite", + "bugs": { + "url": "https://github.com/storybookjs/storybook/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/storybookjs/storybook.git", + "directory": "code/frameworks/react-native-web-vite" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./dist/index.js", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./preset": { + "types": "./dist/preset.d.ts", + "require": "./dist/preset.js" + }, + "./package.json": "./package.json" + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*", + "template/cli/**/*", + "README.md", + "*.js", + "*.d.ts", + "!src/**/*" + ], + "scripts": { + "check": "jiti ../../../scripts/prepare/check.ts", + "prep": "jiti ../../../scripts/prepare/bundle.ts" + }, + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.3.0", + "@storybook/builder-vite": "workspace:*", + "@storybook/react": "workspace:*", + "@storybook/react-vite": "workspace:*", + "@vitejs/plugin-react": "^4.3.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.3.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-native": ">=0.74.5", + "react-native-web": "^0.19.12", + "storybook": "workspace:^", + "vite": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "bundler": { + "entries": [ + "./src/index.ts", + "./src/preset.ts" + ], + "platform": "node" + }, + "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16" +} diff --git a/code/frameworks/react-native-web-vite/preset.js b/code/frameworks/react-native-web-vite/preset.js new file mode 100644 index 000000000000..a83f95279e7f --- /dev/null +++ b/code/frameworks/react-native-web-vite/preset.js @@ -0,0 +1 @@ +module.exports = require('./dist/preset'); diff --git a/code/frameworks/react-native-web-vite/project.json b/code/frameworks/react-native-web-vite/project.json new file mode 100644 index 000000000000..219e9c00077d --- /dev/null +++ b/code/frameworks/react-native-web-vite/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-native-web-vite", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "targets": { + "build": {} + } +} diff --git a/code/frameworks/react-native-web-vite/src/index.ts b/code/frameworks/react-native-web-vite/src/index.ts new file mode 100644 index 000000000000..1855ad61a70b --- /dev/null +++ b/code/frameworks/react-native-web-vite/src/index.ts @@ -0,0 +1 @@ +export type { FrameworkOptions, StorybookConfig } from './types'; diff --git a/code/frameworks/react-native-web-vite/src/preset.ts b/code/frameworks/react-native-web-vite/src/preset.ts new file mode 100644 index 000000000000..e7a4a902bfd1 --- /dev/null +++ b/code/frameworks/react-native-web-vite/src/preset.ts @@ -0,0 +1,91 @@ +// @ts-expect-error FIXME +import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; + +import type { BabelOptions, Options as ReactOptions } from '@vitejs/plugin-react'; +import react from '@vitejs/plugin-react'; +import type { PluginOption } from 'vite'; + +import type { FrameworkOptions, StorybookConfig } from './types'; + +function reactNativeWeb( + reactOptions: Omit & { babel?: BabelOptions } +): PluginOption { + return { + name: 'vite:react-native-web', + config(_userConfig, env) { + return { + define: { + // reanimated support + 'global.__x': {}, + _frameTimestamp: undefined, + _WORKLET: false, + __DEV__: `${env.mode === 'development'}`, + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || env.mode), + }, + optimizeDeps: { + include: [], + esbuildOptions: { + jsx: 'transform', + resolveExtensions: [ + '.web.js', + '.web.ts', + '.web.tsx', + '.js', + '.jsx', + '.json', + '.ts', + '.tsx', + '.mjs', + ], + loader: { + '.js': 'jsx', + }, + }, + }, + resolve: { + extensions: [ + '.web.js', + '.web.ts', + '.web.tsx', + '.js', + '.jsx', + '.json', + '.ts', + '.tsx', + '.mjs', + ], + alias: { + 'react-native': 'react-native-web', + }, + }, + }; + }, + }; +} + +export const viteFinal: StorybookConfig['viteFinal'] = async (config, options) => { + const { pluginReactOptions = {} } = + await options.presets.apply('frameworkOptions'); + + const reactConfig = await reactViteFinal(config, options); + const { plugins = [] } = reactConfig; + + plugins.unshift( + react({ + babel: { + babelrc: false, + configFile: false, + }, + jsxRuntime: 'automatic', + ...pluginReactOptions, + }) + ); + plugins.push(reactNativeWeb(pluginReactOptions)); + + return reactConfig; +}; + +export const core = { + builder: '@storybook/builder-vite', + renderer: '@storybook/react', +}; diff --git a/code/frameworks/react-native-web-vite/src/types.ts b/code/frameworks/react-native-web-vite/src/types.ts new file mode 100644 index 000000000000..c82c79771a1b --- /dev/null +++ b/code/frameworks/react-native-web-vite/src/types.ts @@ -0,0 +1,24 @@ +import type { CompatibleString } from 'storybook/internal/types'; + +import type { + FrameworkOptions as FrameworkOptionsBase, + StorybookConfig as StorybookConfigBase, +} from '@storybook/react-vite'; + +import type { BabelOptions, Options as ReactOptions } from '@vitejs/plugin-react'; + +export type FrameworkOptions = FrameworkOptionsBase & { + pluginReactOptions?: Omit & { babel?: BabelOptions }; +}; + +type FrameworkName = CompatibleString<'@storybook/react-native-web-vite'>; + +/** The interface for Storybook configuration in `main.ts` files. */ +export type StorybookConfig = Omit & { + framework: + | FrameworkName + | { + name: FrameworkName; + options: FrameworkOptions; + }; +}; diff --git a/code/frameworks/react-native-web-vite/template/cli/.eslintrc.json b/code/frameworks/react-native-web-vite/template/cli/.eslintrc.json new file mode 100644 index 000000000000..3715384e9511 --- /dev/null +++ b/code/frameworks/react-native-web-vite/template/cli/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "import/extensions": "off", + "react/no-unknown-property": "off", + "react/react-in-jsx-scope": "off" + } +} diff --git a/code/frameworks/react-native-web-vite/template/cli/js/Button.jsx b/code/frameworks/react-native-web-vite/template/cli/js/Button.jsx new file mode 100644 index 000000000000..4d87b6e2c4ea --- /dev/null +++ b/code/frameworks/react-native-web-vite/template/cli/js/Button.jsx @@ -0,0 +1,109 @@ +import PropTypes from 'prop-types'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +/** Primary UI component for user interaction */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + style, + onPress, +}) => { + const modeStyle = primary ? styles.primary : styles.secondary; + const textModeStyle = primary ? styles.primaryText : styles.secondaryText; + + const sizeStyle = styles[size]; + const textSizeStyle = textSizeStyles[size]; + + return ( + + + {label} + + + ); +}; + +const styles = StyleSheet.create({ + button: { + borderWidth: 0, + borderRadius: 48, + }, + buttonText: { + fontWeight: '700', + lineHeight: 1, + }, + primary: { + backgroundColor: '#1ea7fd', + }, + primaryText: { + color: 'white', + }, + secondary: { + backgroundColor: 'transparent', + borderColor: 'rgba(0, 0, 0, 0.15)', + borderWidth: 1, + }, + secondaryText: { + color: '#333', + }, + small: { + paddingVertical: 10, + paddingHorizontal: 16, + }, + smallText: { + fontSize: 12, + }, + medium: { + paddingVertical: 11, + paddingHorizontal: 20, + }, + mediumText: { + fontSize: 14, + }, + large: { + paddingVertical: 12, + paddingHorizontal: 24, + }, + largeText: { + fontSize: 16, + }, +}); + +const textSizeStyles = { + small: styles.smallText, + medium: styles.mediumText, + large: styles.largeText, +}; + +Button.propTypes = { + /** Is this the principal call to action on the page? */ + primary: PropTypes.bool, + /** What background color to use */ + backgroundColor: PropTypes.string, + /** How large should the button be? */ + size: PropTypes.oneOf(['small', 'medium', 'large']), + /** Button contents */ + label: PropTypes.string.isRequired, + /** Optional click handler */ + onPress: PropTypes.func, + /** Optional extra styles */ + style: PropTypes.object, +}; + +Button.defaultProps = { + backgroundColor: null, + primary: false, + size: 'medium', + onClick: undefined, +}; diff --git a/code/frameworks/react-native-web-vite/template/cli/js/Button.stories.jsx b/code/frameworks/react-native-web-vite/template/cli/js/Button.stories.jsx new file mode 100644 index 000000000000..b7136fd8ae67 --- /dev/null +++ b/code/frameworks/react-native-web-vite/template/cli/js/Button.stories.jsx @@ -0,0 +1,50 @@ +import { fn } from '@storybook/test'; + +import { View } from 'react-native'; + +import { Button } from './Button'; + +const meta = { + title: 'Example/Button', + component: Button, + decorators: [ + (Story) => ( + + + + ), + ], + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // Use `fn` to spy on the onPress arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onPress: fn() }, +}; + +export default meta; + +export const Primary = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary = { + args: { + label: 'Button', + }, +}; + +export const Large = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/code/frameworks/react-native-web-vite/template/cli/js/Header.jsx b/code/frameworks/react-native-web-vite/template/cli/js/Header.jsx new file mode 100644 index 000000000000..ebc34b85ea4f --- /dev/null +++ b/code/frameworks/react-native-web-vite/template/cli/js/Header.jsx @@ -0,0 +1,84 @@ +import PropTypes from 'prop-types'; +import { StyleSheet, Text, View } from 'react-native'; + +import { Button } from './Button'; + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => ( + + + + Acme + + + + {user ? ( + <> + <> + Welcome, + {user.name}! + +