diff --git a/.circleci/config.yml b/.circleci/config.yml
index 18ad245f250b..9dce34f6faf6 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -257,7 +257,7 @@ jobs:
# executor:
# class: large
# name: sb_cypress_8_node_14
- # parallelism: 2
+ # parallelism: 8
# steps:
# - git-shallow-clone/checkout_advanced:
# clone_options: '--depth 1 --verbose'
@@ -462,7 +462,7 @@ jobs:
executor:
class: medium+
name: sb_node_14_browsers
- parallelism: 2
+ parallelism: 6
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
@@ -482,7 +482,7 @@ jobs:
executor:
class: medium+
name: sb_node_14_browsers
- parallelism: 2
+ parallelism: 6
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
@@ -498,7 +498,7 @@ jobs:
executor:
class: medium+
name: sb_node_14_browsers
- parallelism: 2
+ parallelism: 6
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
@@ -518,7 +518,7 @@ jobs:
executor:
class: medium+
name: sb_node_14_browsers
- parallelism: 2
+ parallelism: 6
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
@@ -534,7 +534,7 @@ jobs:
executor:
class: medium+
name: sb_node_14_browsers
- parallelism: 2
+ parallelism: 6
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
@@ -549,7 +549,7 @@ jobs:
e2e-sandboxes:
docker:
- image: mcr.microsoft.com/playwright:v1.24.0-focal
- parallelism: 2
+ parallelism: 6
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
diff --git a/.github/workflows/generate-repros-next.yml b/.github/workflows/generate-repros-next.yml
index a1774867b6c0..95dfdd803c8f 100644
--- a/.github/workflows/generate-repros-next.yml
+++ b/.github/workflows/generate-repros-next.yml
@@ -7,7 +7,7 @@ on:
# To remove when the branch will be merged
push:
branches:
- - shilman/add-angular-repro-template
+ - vite-frameworks
jobs:
generate:
@@ -15,6 +15,9 @@ jobs:
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: false
steps:
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
- uses: actions/checkout@v2
- name: Setup git user
run: |
diff --git a/.github/workflows/generate-repros.yml b/.github/workflows/generate-repros.yml
index 43b11ccfcce2..d66fbbe37244 100644
--- a/.github/workflows/generate-repros.yml
+++ b/.github/workflows/generate-repros.yml
@@ -15,9 +15,6 @@ jobs:
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: false
steps:
- - uses: actions/setup-node@v2
- with:
- node-version: 14
- uses: actions/checkout@v2
- name: Setup git user
run: |
diff --git a/code/.eslintrc.js b/code/.eslintrc.js
index 1129908e7eb0..0e590a380807 100644
--- a/code/.eslintrc.js
+++ b/code/.eslintrc.js
@@ -101,5 +101,11 @@ module.exports = {
'jest/no-test-callback': 'off', // These aren't jest tests
},
},
+ {
+ files: ['**/builder-vite/input/iframe.html'],
+ rules: {
+ 'no-undef': 'off', // ignore "window" undef errors
+ },
+ },
],
};
diff --git a/code/.vscode/settings.json b/code/.vscode/settings.json
new file mode 100644
index 000000000000..25fa6215fdd3
--- /dev/null
+++ b/code/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json
index a2c3e076e0bc..1ad99f71662e 100644
--- a/code/addons/a11y/package.json
+++ b/code/addons/a11y/package.json
@@ -31,12 +31,12 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
},
- "./preview": {
+ "./preview.js": {
"require": "./dist/preview.js",
"import": "./dist/preview.mjs",
"types": "./dist/preview.d.ts"
diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json
index 40ea81c45637..e3c1c838d6e0 100644
--- a/code/addons/backgrounds/package.json
+++ b/code/addons/backgrounds/package.json
@@ -31,12 +31,12 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
},
- "./preview": {
+ "./preview.js": {
"require": "./dist/preview.js",
"import": "./dist/preview.mjs",
"types": "./dist/preview.d.ts"
diff --git a/code/addons/controls/package.json b/code/addons/controls/package.json
index 65207a7489c0..174f7ccfe4c3 100644
--- a/code/addons/controls/package.json
+++ b/code/addons/controls/package.json
@@ -31,7 +31,7 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json
index df0006d8a35b..f62e9e54a01a 100644
--- a/code/addons/jest/package.json
+++ b/code/addons/jest/package.json
@@ -33,7 +33,7 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
diff --git a/code/addons/links/package.json b/code/addons/links/package.json
index 8ef24d9900ab..5f390fec36d9 100644
--- a/code/addons/links/package.json
+++ b/code/addons/links/package.json
@@ -27,12 +27,12 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
},
- "./preview": {
+ "./preview.js": {
"require": "./dist/preview.js",
"import": "./dist/preview.mjs",
"types": "./dist/preview.d.ts"
diff --git a/code/addons/measure/package.json b/code/addons/measure/package.json
index d282049ee859..8cdf1701a339 100644
--- a/code/addons/measure/package.json
+++ b/code/addons/measure/package.json
@@ -30,12 +30,12 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
},
- "./preview": {
+ "./preview.js": {
"require": "./dist/preview.js",
"import": "./dist/preview.mjs",
"types": "./dist/preview.d.ts"
diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json
index 252956864af8..6dc9a1e89a47 100644
--- a/code/addons/outline/package.json
+++ b/code/addons/outline/package.json
@@ -33,12 +33,12 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
},
- "./preview": {
+ "./preview.js": {
"require": "./dist/preset/preview.js",
"import": "./dist/preset/preview.mjs",
"types": "./dist/preview.d.ts"
diff --git a/code/addons/toolbars/package.json b/code/addons/toolbars/package.json
index abd624753148..1a069ad18f04 100644
--- a/code/addons/toolbars/package.json
+++ b/code/addons/toolbars/package.json
@@ -31,7 +31,7 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
- "./manager": {
+ "./manager.js": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
diff --git a/code/examples/vite-react-app/.eslintrc.js b/code/examples/vite-react-app/.eslintrc.js
new file mode 100644
index 000000000000..593bd9708109
--- /dev/null
+++ b/code/examples/vite-react-app/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ extends: ['../../.eslintrc.js'],
+ env: {
+ browser: true,
+ },
+ rules: {
+ 'import/extensions': [0],
+ 'import/no-unresolved': [0],
+ 'react/react-in-jsx-scope': [0],
+ },
+};
diff --git a/code/examples/vite-react-app/.gitignore b/code/examples/vite-react-app/.gitignore
new file mode 100644
index 000000000000..a547bf36d8d1
--- /dev/null
+++ b/code/examples/vite-react-app/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/code/examples/vite-react-app/.storybook/main.cjs b/code/examples/vite-react-app/.storybook/main.cjs
new file mode 100644
index 000000000000..18cfb9773cec
--- /dev/null
+++ b/code/examples/vite-react-app/.storybook/main.cjs
@@ -0,0 +1,14 @@
+module.exports = {
+ stories: [
+ // '../src/**/*.stories.mdx',
+ '../src/**/*.stories.@(js|jsx|ts|tsx)',
+ ],
+ addons: ['@storybook/addon-essentials'],
+ framework: '@storybook/react-vite',
+ features: {
+ storyStoreV7: true,
+ },
+ docs: {
+ docsPage: false, // set to false to disable docs page entirely
+ },
+};
diff --git a/code/examples/vite-react-app/.storybook/preview-head.html b/code/examples/vite-react-app/.storybook/preview-head.html
new file mode 100644
index 000000000000..05da1e9dfbfe
--- /dev/null
+++ b/code/examples/vite-react-app/.storybook/preview-head.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/code/examples/vite-react-app/.storybook/preview.js b/code/examples/vite-react-app/.storybook/preview.js
new file mode 100644
index 000000000000..d3914580a724
--- /dev/null
+++ b/code/examples/vite-react-app/.storybook/preview.js
@@ -0,0 +1,9 @@
+export const parameters = {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+};
diff --git a/code/examples/vite-react-app/index.html b/code/examples/vite-react-app/index.html
new file mode 100644
index 000000000000..b46ab83364e3
--- /dev/null
+++ b/code/examples/vite-react-app/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/code/examples/vite-react-app/package.json b/code/examples/vite-react-app/package.json
new file mode 100644
index 000000000000..5585eb63af29
--- /dev/null
+++ b/code/examples/vite-react-app/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@storybook/example-vite-react-app",
+ "version": "7.0.0-alpha.23",
+ "scripts": {
+ "build": "vite build",
+ "build-storybook": "STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true sb build",
+ "dev": "vite",
+ "preview": "vite preview",
+ "storybook": "STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true sb dev --no-manager-cache"
+ },
+ "dependencies": {
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "devDependencies": {
+ "@mdx-js/preact": "^1.6.0",
+ "@mdx-js/react": "^1.6.0",
+ "@storybook/addon-essentials": "7.0.0-alpha.23",
+ "@storybook/builder-vite": "7.0.0-alpha.23",
+ "@storybook/react": "7.0.0-alpha.23",
+ "@storybook/react-vite": "7.0.0-alpha.23",
+ "@vitejs/plugin-react": "^1.0.7",
+ "prop-types": "^15.8.1",
+ "sb": "7.0.0-alpha.23",
+ "vite": "^2.9.0"
+ }
+}
diff --git a/code/examples/vite-react-app/src/App.css b/code/examples/vite-react-app/src/App.css
new file mode 100644
index 000000000000..8da3fde63d9e
--- /dev/null
+++ b/code/examples/vite-react-app/src/App.css
@@ -0,0 +1,42 @@
+.App {
+ text-align: center;
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ background-color: #282c34;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+button {
+ font-size: calc(10px + 2vmin);
+}
diff --git a/code/examples/vite-react-app/src/App.jsx b/code/examples/vite-react-app/src/App.jsx
new file mode 100644
index 000000000000..28cea7c891ff
--- /dev/null
+++ b/code/examples/vite-react-app/src/App.jsx
@@ -0,0 +1,46 @@
+/* eslint-disable no-shadow */
+import { useState } from 'react';
+import logo from './logo.svg';
+import './App.css';
+
+function App() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
+
+ Hello Vite + React!
+
+ setCount((count) => count + 1)}>
+ count is: {count}
+
+
+
+ Edit App.jsx
and save to test HMR updates.
+
+
+
+ Learn React
+
+ {' | '}
+
+ Vite Docs
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/code/examples/vite-react-app/src/favicon.svg b/code/examples/vite-react-app/src/favicon.svg
new file mode 100644
index 000000000000..de4aeddc12bd
--- /dev/null
+++ b/code/examples/vite-react-app/src/favicon.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/code/examples/vite-react-app/src/index.css b/code/examples/vite-react-app/src/index.css
new file mode 100644
index 000000000000..ec2585e8c0bb
--- /dev/null
+++ b/code/examples/vite-react-app/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/code/examples/vite-react-app/src/logo.svg b/code/examples/vite-react-app/src/logo.svg
new file mode 100644
index 000000000000..6b60c1042f58
--- /dev/null
+++ b/code/examples/vite-react-app/src/logo.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/code/examples/vite-react-app/src/main.jsx b/code/examples/vite-react-app/src/main.jsx
new file mode 100644
index 000000000000..6832e7832bb9
--- /dev/null
+++ b/code/examples/vite-react-app/src/main.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+);
diff --git a/code/examples/vite-react-app/src/stories/Button.jsx b/code/examples/vite-react-app/src/stories/Button.jsx
new file mode 100644
index 000000000000..15dde3920956
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Button.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import './button.css';
+
+/**
+ * Primary UI component for user interaction
+ */
+export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
+ const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
+ return (
+
+ {label}
+
+ );
+};
+
+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
+ */
+ onClick: PropTypes.func,
+};
+
+Button.defaultProps = {
+ backgroundColor: null,
+ primary: false,
+ size: 'medium',
+ onClick: undefined,
+};
diff --git a/code/examples/vite-react-app/src/stories/Button.stories.jsx b/code/examples/vite-react-app/src/stories/Button.stories.jsx
new file mode 100644
index 000000000000..61f6e19e14d7
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Button.stories.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { Button } from './Button';
+
+// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
+export default {
+ title: 'Example/Button',
+ component: Button,
+ // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+};
+
+// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
+const Template = (args) => ;
+
+export const Primary = Template.bind({});
+// More on args: https://storybook.js.org/docs/react/writing-stories/args
+Primary.args = {
+ primary: true,
+ label: 'Button',
+};
+
+export const Secondary = Template.bind({});
+Secondary.args = {
+ label: 'Button',
+};
+
+export const Large = Template.bind({});
+Large.args = {
+ size: 'large',
+ label: 'Button',
+};
+
+export const Small = Template.bind({});
+Small.args = {
+ size: 'small',
+ label: 'Button',
+};
diff --git a/code/examples/vite-react-app/src/stories/Header.jsx b/code/examples/vite-react-app/src/stories/Header.jsx
new file mode 100644
index 000000000000..e7b8b93d725b
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Header.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Button } from './Button';
+import './header.css';
+
+export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => (
+
+);
+
+Header.propTypes = {
+ user: PropTypes.shape({
+ name: PropTypes.string,
+ }),
+ onLogin: PropTypes.func.isRequired,
+ onLogout: PropTypes.func.isRequired,
+ onCreateAccount: PropTypes.func.isRequired,
+};
+
+Header.defaultProps = {
+ user: null,
+};
diff --git a/code/examples/vite-react-app/src/stories/Header.stories.jsx b/code/examples/vite-react-app/src/stories/Header.stories.jsx
new file mode 100644
index 000000000000..e4850002cac5
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Header.stories.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { Header } from './Header';
+
+export default {
+ title: 'Example/Header',
+ component: Header,
+ parameters: {
+ // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
+ layout: 'fullscreen',
+ },
+};
+
+const Template = (args) => ;
+
+export const LoggedIn = Template.bind({});
+LoggedIn.args = {
+ user: {
+ name: 'Jane Doe',
+ },
+};
+
+export const LoggedOut = Template.bind({});
+LoggedOut.args = {};
diff --git a/code/examples/vite-react-app/src/stories/Introduction.stories.mdx b/code/examples/vite-react-app/src/stories/Introduction.stories.mdx
new file mode 100644
index 000000000000..42c4a8714eee
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Introduction.stories.mdx
@@ -0,0 +1,211 @@
+import { Meta } from '@storybook/addon-docs';
+import Code from './assets/code-brackets.svg';
+import Colors from './assets/colors.svg';
+import Comments from './assets/comments.svg';
+import Direction from './assets/direction.svg';
+import Flow from './assets/flow.svg';
+import Plugin from './assets/plugin.svg';
+import Repo from './assets/repo.svg';
+import StackAlt from './assets/stackalt.svg';
+
+
+
+
+
+# Welcome to Storybook
+
+Storybook helps you build UI components in isolation from your app's business logic, data, and context.
+That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
+
+Browse example stories now by navigating to them in the sidebar.
+View their code in the `src/stories` directory to learn how they work.
+We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
+
+Configure
+
+
+
+Learn
+
+
+
+
+ Tip Edit the Markdown in{' '}
+ src/stories/Introduction.stories.mdx
+
diff --git a/code/examples/vite-react-app/src/stories/Page.jsx b/code/examples/vite-react-app/src/stories/Page.jsx
new file mode 100644
index 000000000000..c5fffe953be5
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Page.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+
+import { Header } from './Header';
+import './page.css';
+
+export const Page = () => {
+ const [user, setUser] = React.useState();
+
+ return (
+
+ setUser({ name: 'Jane Doe' })}
+ onLogout={() => setUser(undefined)}
+ onCreateAccount={() => setUser({ name: 'Jane Doe' })}
+ />
+
+
+ Pages in Storybook
+
+ We recommend building UIs with a{' '}
+
+ component-driven
+ {' '}
+ process starting with atomic components and ending with pages.
+
+
+ Render pages with mock data. This makes it easy to build and review page states without
+ needing to navigate to them in your app. Here are some handy patterns for managing page
+ data in Storybook:
+
+
+
+ Use a higher-level connected component. Storybook helps you compose such data from the
+ "args" of child component stories
+
+
+ Assemble data in the page component from your services. You can mock these services out
+ using Storybook.
+
+
+
+ Get a guided tutorial on component-driven development at{' '}
+
+ Storybook tutorials
+
+ . Read more in the{' '}
+
+ docs
+
+ .
+
+
+
Tip Adjust the width of the canvas with the{' '}
+
+
+
+
+
+ Viewports addon in the toolbar
+
+
+
+ );
+};
diff --git a/code/examples/vite-react-app/src/stories/Page.stories.jsx b/code/examples/vite-react-app/src/stories/Page.stories.jsx
new file mode 100644
index 000000000000..0174fdb881f9
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/Page.stories.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { within, userEvent } from '@storybook/testing-library';
+
+import { Page } from './Page';
+
+export default {
+ title: 'Example/Page',
+ component: Page,
+ parameters: {
+ // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
+ layout: 'fullscreen',
+ },
+};
+
+const Template = (args) => ;
+
+// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing
+export const LoggedOut = Template.bind({});
+
+export const LoggedIn = Template.bind({});
+LoggedIn.play = async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const loginButton = await canvas.getByRole('button', { name: /Log in/i });
+ await userEvent.click(loginButton);
+};
diff --git a/code/examples/vite-react-app/src/stories/assets/code-brackets.svg b/code/examples/vite-react-app/src/stories/assets/code-brackets.svg
new file mode 100644
index 000000000000..73de94776001
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/code-brackets.svg
@@ -0,0 +1 @@
+illustration/code-brackets
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/colors.svg b/code/examples/vite-react-app/src/stories/assets/colors.svg
new file mode 100644
index 000000000000..17d58d516e14
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/colors.svg
@@ -0,0 +1 @@
+illustration/colors
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/comments.svg b/code/examples/vite-react-app/src/stories/assets/comments.svg
new file mode 100644
index 000000000000..6493a139f523
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/comments.svg
@@ -0,0 +1 @@
+illustration/comments
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/direction.svg b/code/examples/vite-react-app/src/stories/assets/direction.svg
new file mode 100644
index 000000000000..65676ac27229
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/direction.svg
@@ -0,0 +1 @@
+illustration/direction
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/flow.svg b/code/examples/vite-react-app/src/stories/assets/flow.svg
new file mode 100644
index 000000000000..8ac27db403c2
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/flow.svg
@@ -0,0 +1 @@
+illustration/flow
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/plugin.svg b/code/examples/vite-react-app/src/stories/assets/plugin.svg
new file mode 100644
index 000000000000..29e5c690c0a2
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/plugin.svg
@@ -0,0 +1 @@
+illustration/plugin
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/repo.svg b/code/examples/vite-react-app/src/stories/assets/repo.svg
new file mode 100644
index 000000000000..f386ee902c1f
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/repo.svg
@@ -0,0 +1 @@
+illustration/repo
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/assets/stackalt.svg b/code/examples/vite-react-app/src/stories/assets/stackalt.svg
new file mode 100644
index 000000000000..9b7ad2743506
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/assets/stackalt.svg
@@ -0,0 +1 @@
+illustration/stackalt
\ No newline at end of file
diff --git a/code/examples/vite-react-app/src/stories/button.css b/code/examples/vite-react-app/src/stories/button.css
new file mode 100644
index 000000000000..dc91dc76370b
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/button.css
@@ -0,0 +1,30 @@
+.storybook-button {
+ font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ border: 0;
+ border-radius: 3em;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 1;
+}
+.storybook-button--primary {
+ color: white;
+ background-color: #1ea7fd;
+}
+.storybook-button--secondary {
+ color: #333;
+ background-color: transparent;
+ box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
+}
+.storybook-button--small {
+ font-size: 12px;
+ padding: 10px 16px;
+}
+.storybook-button--medium {
+ font-size: 14px;
+ padding: 11px 20px;
+}
+.storybook-button--large {
+ font-size: 16px;
+ padding: 12px 24px;
+}
diff --git a/code/examples/vite-react-app/src/stories/header.css b/code/examples/vite-react-app/src/stories/header.css
new file mode 100644
index 000000000000..830610e6f2e9
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/header.css
@@ -0,0 +1,32 @@
+.wrapper {
+ font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 15px 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+svg {
+ display: inline-block;
+ vertical-align: top;
+}
+
+h1 {
+ font-weight: 900;
+ font-size: 20px;
+ line-height: 1;
+ margin: 6px 0 6px 10px;
+ display: inline-block;
+ vertical-align: top;
+}
+
+button + button {
+ margin-left: 10px;
+}
+
+.welcome {
+ color: #333;
+ font-size: 14px;
+ margin-right: 10px;
+}
diff --git a/code/examples/vite-react-app/src/stories/page.css b/code/examples/vite-react-app/src/stories/page.css
new file mode 100644
index 000000000000..fbc32aea2e0f
--- /dev/null
+++ b/code/examples/vite-react-app/src/stories/page.css
@@ -0,0 +1,69 @@
+section {
+ font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 24px;
+ padding: 48px 20px;
+ margin: 0 auto;
+ max-width: 600px;
+ color: #333;
+}
+
+section h2 {
+ font-weight: 900;
+ font-size: 32px;
+ line-height: 1;
+ margin: 0 0 4px;
+ display: inline-block;
+ vertical-align: top;
+}
+
+section p {
+ margin: 1em 0;
+}
+
+section a {
+ text-decoration: none;
+ color: #1ea7fd;
+}
+
+section ul {
+ padding-left: 30px;
+ margin: 1em 0;
+}
+
+section li {
+ margin-bottom: 8px;
+}
+
+section .tip {
+ display: inline-block;
+ border-radius: 1em;
+ font-size: 11px;
+ line-height: 12px;
+ font-weight: 700;
+ background: #e7fdd8;
+ color: #66bf3c;
+ padding: 4px 12px;
+ margin-right: 10px;
+ vertical-align: top;
+}
+
+section .tip-wrapper {
+ font-size: 13px;
+ line-height: 20px;
+ margin-top: 40px;
+ margin-bottom: 40px;
+}
+
+section .tip-wrapper svg {
+ display: inline-block;
+ height: 12px;
+ width: 12px;
+ margin-right: 4px;
+ vertical-align: top;
+ margin-top: 3px;
+}
+
+section .tip-wrapper svg path {
+ fill: #1ea7fd;
+}
diff --git a/code/examples/vite-react-app/vite.config.js b/code/examples/vite-react-app/vite.config.js
new file mode 100644
index 000000000000..627a3196243d
--- /dev/null
+++ b/code/examples/vite-react-app/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/code/frameworks/react-vite/README.md b/code/frameworks/react-vite/README.md
new file mode 100644
index 000000000000..e8a35450aec9
--- /dev/null
+++ b/code/frameworks/react-vite/README.md
@@ -0,0 +1 @@
+# Storybook for React
diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json
new file mode 100644
index 000000000000..c79b4624a48b
--- /dev/null
+++ b/code/frameworks/react-vite/package.json
@@ -0,0 +1,99 @@
+{
+ "name": "@storybook/react-vite",
+ "version": "7.0.0-alpha.23",
+ "description": "Storybook for React: Develop React Component in isolation with Hot Reloading.",
+ "keywords": [
+ "storybook"
+ ],
+ "homepage": "https://github.com/storybookjs/storybook/tree/main/frameworks/react-vite",
+ "bugs": {
+ "url": "https://github.com/storybookjs/storybook/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybookjs/storybook.git",
+ "directory": "frameworks/react-vite"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "license": "MIT",
+ "exports": {
+ ".": {
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs",
+ "types": "./dist/index.d.ts"
+ },
+ "./preset": {
+ "require": "./dist/preset.js",
+ "import": "./dist/preset.mjs",
+ "types": "./dist/preset.d.ts"
+ },
+ "./package.json": {
+ "require": "./package.json",
+ "import": "./package.json",
+ "types": "./package.json"
+ }
+ },
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist/**/*",
+ "types/**/*",
+ "README.md",
+ "*.js",
+ "*.d.ts"
+ ],
+ "scripts": {
+ "check": "tsc --noEmit",
+ "prepare": "../../../scripts/prepare/bundle.ts"
+ },
+ "dependencies": {
+ "@joshwooding/vite-plugin-react-docgen-typescript": "0.0.4",
+ "@rollup/pluginutils": "^4.2.0",
+ "@storybook/builder-vite": "7.0.0-alpha.23",
+ "@storybook/core-server": "7.0.0-alpha.23",
+ "@storybook/core-vite": "7.0.0-alpha.23",
+ "@storybook/react": "7.0.0-alpha.23",
+ "@types/node": "^14.14.20 || ^16.0.0",
+ "@vitejs/plugin-react": "^1.0.8",
+ "ast-types": "^0.14.2",
+ "core-js": "^3.8.2",
+ "magic-string": "^0.26.1",
+ "react-docgen": "6.0.0-alpha.1",
+ "regenerator-runtime": "^0.13.7"
+ },
+ "devDependencies": {
+ "jest-specific-snapshot": "^4.0.0",
+ "typescript": "~4.6.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.5",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bundler": {
+ "entries": [
+ "./src/index.ts",
+ "./src/preset.ts"
+ ],
+ "platform": "node"
+ },
+ "gitHead": "55247a8e36da7061bfced80c588a539d3fda3f04"
+}
diff --git a/code/frameworks/react-vite/preset.js b/code/frameworks/react-vite/preset.js
new file mode 100644
index 000000000000..a83f95279e7f
--- /dev/null
+++ b/code/frameworks/react-vite/preset.js
@@ -0,0 +1 @@
+module.exports = require('./dist/preset');
diff --git a/code/frameworks/react-vite/src/index.ts b/code/frameworks/react-vite/src/index.ts
new file mode 100644
index 000000000000..da28051ed337
--- /dev/null
+++ b/code/frameworks/react-vite/src/index.ts
@@ -0,0 +1 @@
+export * from '@storybook/react';
diff --git a/code/frameworks/react-vite/src/plugins/docgen-handlers/actualNameHandler.ts b/code/frameworks/react-vite/src/plugins/docgen-handlers/actualNameHandler.ts
new file mode 100644
index 000000000000..4849df62d087
--- /dev/null
+++ b/code/frameworks/react-vite/src/plugins/docgen-handlers/actualNameHandler.ts
@@ -0,0 +1,48 @@
+/**
+ * This is heavily based on the react-docgen `displayNameHandler`
+ * (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
+ * but instead defines an `actualName` property on the generated docs that is taken first from the component's actual name.
+ * This addresses an issue where the name that the generated docs are stored under is incorrectly named with the `displayName`
+ * and not the component's actual name.
+ *
+ * This is inspired by `actualNameHandler` from https://github.com/storybookjs/babel-plugin-react-docgen, but is modified
+ * directly from displayNameHandler, using the same approach as babel-plugin-react-docgen.
+ */
+
+import { namedTypes as t } from 'ast-types';
+import type { NodePath } from 'ast-types/lib/node-path';
+import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/lib/utils';
+import type { Importer } from 'react-docgen/lib/parse';
+import type Documentation from 'react-docgen/lib/Documentation';
+
+export default function actualNameHandler(
+ documentation: Documentation,
+ path: NodePath,
+ importer: Importer
+): void {
+ if (t.ClassDeclaration.check(path.node) || t.FunctionDeclaration.check(path.node)) {
+ documentation.set('actualName', getNameOrValue(path.get('id')));
+ } else if (
+ t.ArrowFunctionExpression.check(path.node) ||
+ t.FunctionExpression.check(path.node) ||
+ isReactForwardRefCall(path, importer)
+ ) {
+ let currentPath = path;
+ while (currentPath.parent) {
+ if (t.VariableDeclarator.check(currentPath.parent.node)) {
+ documentation.set('actualName', getNameOrValue(currentPath.parent.get('id')));
+ return;
+ }
+ if (t.AssignmentExpression.check(currentPath.parent.node)) {
+ const leftPath = currentPath.parent.get('left');
+ if (t.Identifier.check(leftPath.node) || t.Literal.check(leftPath.node)) {
+ documentation.set('actualName', getNameOrValue(leftPath));
+ return;
+ }
+ }
+ currentPath = currentPath.parent;
+ }
+ // Could not find an actual name
+ documentation.set('actualName', '');
+ }
+}
diff --git a/code/frameworks/react-vite/src/plugins/react-docgen.ts b/code/frameworks/react-vite/src/plugins/react-docgen.ts
new file mode 100644
index 000000000000..699bf30c2745
--- /dev/null
+++ b/code/frameworks/react-vite/src/plugins/react-docgen.ts
@@ -0,0 +1,69 @@
+import path from 'path';
+import { createFilter } from '@rollup/pluginutils';
+import {
+ parse,
+ handlers as docgenHandlers,
+ resolver as docgenResolver,
+ importers as docgenImporters,
+} from 'react-docgen';
+import type { DocumentationObject } from 'react-docgen/lib/Documentation';
+import MagicString from 'magic-string';
+import type { Plugin } from 'vite';
+import actualNameHandler from './docgen-handlers/actualNameHandler';
+
+type DocObj = DocumentationObject & { actualName: string };
+
+// TODO: None of these are able to be overridden, so `default` is aspirational here.
+const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler);
+const defaultResolver = docgenResolver.findAllExportedComponentDefinitions;
+const defaultImporter = docgenImporters.makeFsImporter();
+const handlers = [...defaultHandlers, actualNameHandler];
+
+type Options = {
+ include?: string | RegExp | (string | RegExp)[];
+ exclude?: string | RegExp | (string | RegExp)[];
+};
+
+export function reactDocgen({
+ include = /\.(mjs|tsx?|jsx?)$/,
+ exclude = [/node_modules\/.*/],
+}: Options = {}): Plugin {
+ const cwd = process.cwd();
+ const filter = createFilter(include, exclude);
+
+ return {
+ name: 'react-docgen',
+ enforce: 'pre',
+ async transform(src: string, id: string) {
+ const relPath = path.relative(cwd, id);
+ if (!filter(relPath)) return;
+
+ try {
+ // Since we're using `findAllExportedComponentDefinitions`, this will always be an array.
+ const docgenResults = parse(src, defaultResolver, handlers, {
+ importer: defaultImporter,
+ filename: id,
+ }) as DocObj[];
+ const s = new MagicString(src);
+
+ docgenResults.forEach((info) => {
+ const { actualName, ...docgenInfo } = info;
+ if (actualName) {
+ const docNode = JSON.stringify(docgenInfo);
+ s.append(`;${actualName}.__docgenInfo=${docNode}`);
+ }
+ });
+
+ // eslint-disable-next-line consistent-return
+ return {
+ code: s.toString(),
+ map: s.generateMap(),
+ };
+ } catch (e) {
+ // Usually this is just an error from react-docgen that it couldn't find a component
+ // Only uncomment for troubleshooting
+ // console.error(e);
+ }
+ },
+ };
+}
diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts
new file mode 100644
index 000000000000..5657a7280cba
--- /dev/null
+++ b/code/frameworks/react-vite/src/preset.ts
@@ -0,0 +1,52 @@
+/* eslint-disable global-require */
+import path from 'path';
+import fs from 'fs';
+import type { StorybookConfig, TypescriptOptions } from '@storybook/core-vite';
+
+export const addons: StorybookConfig['addons'] = ['@storybook/react'];
+
+export const core: StorybookConfig['core'] = {
+ builder: '@storybook/builder-vite',
+};
+
+export function readPackageJson(): Record | false {
+ const packageJsonPath = path.resolve('package.json');
+ if (!fs.existsSync(packageJsonPath)) {
+ return false;
+ }
+
+ const jsonContent = fs.readFileSync(packageJsonPath, 'utf8');
+ return JSON.parse(jsonContent);
+}
+
+export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => {
+ const { plugins = [] } = config;
+
+ const { reactDocgen, reactDocgenTypescriptOptions } = await presets.apply(
+ 'typescript',
+ {} as TypescriptOptions
+ );
+ let typescriptPresent;
+
+ try {
+ const pkgJson = readPackageJson();
+ typescriptPresent =
+ pkgJson && (pkgJson.devDependencies?.typescript || pkgJson.dependencies?.typescript);
+ } catch (e) {
+ typescriptPresent = false;
+ }
+
+ if (reactDocgen === 'react-docgen-typescript' && typescriptPresent) {
+ plugins.push(
+ require('@joshwooding/vite-plugin-react-docgen-typescript').default(
+ reactDocgenTypescriptOptions
+ )
+ );
+ } else if (reactDocgen) {
+ const { reactDocgen } = await import('./plugins/react-docgen');
+ // Needs to run before the react plugin, so add to the front
+ plugins.unshift(reactDocgen());
+ }
+
+ return config;
+};
diff --git a/code/frameworks/react-vite/src/typings.d.ts b/code/frameworks/react-vite/src/typings.d.ts
new file mode 100644
index 000000000000..f21e29cca314
--- /dev/null
+++ b/code/frameworks/react-vite/src/typings.d.ts
@@ -0,0 +1,46 @@
+declare module '@storybook/semver';
+declare module 'global';
+
+// TODO: Replace, as soon as @types/react-dom 17.0.14 is used
+// Source: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fb0f14b7a35cde26ffaa82e7536c062e593e9ae6/types/react-dom/client.d.ts
+declare module 'react-dom/client' {
+ import React = require('react');
+ export interface HydrationOptions {
+ onHydrated?(suspenseInstance: Comment): void;
+ onDeleted?(suspenseInstance: Comment): void;
+ /**
+ * Prefix for `useId`.
+ */
+ identifierPrefix?: string;
+ onRecoverableError?: (error: unknown) => void;
+ }
+
+ export interface RootOptions {
+ /**
+ * Prefix for `useId`.
+ */
+ identifierPrefix?: string;
+ onRecoverableError?: (error: unknown) => void;
+ }
+
+ export interface Root {
+ render(children: React.ReactChild | Iterable): void;
+ unmount(): void;
+ }
+
+ /**
+ * Replaces `ReactDOM.render` when the `.render` method is called and enables Concurrent Mode.
+ *
+ * @see https://reactjs.org/docs/concurrent-mode-reference.html#createroot
+ */
+ export function createRoot(
+ container: Element | Document | DocumentFragment | Comment,
+ options?: RootOptions
+ ): Root;
+
+ export function hydrateRoot(
+ container: Element | Document | DocumentFragment | Comment,
+ initialChildren: React.ReactChild | Iterable,
+ options?: HydrationOptions
+ ): Root;
+}
diff --git a/code/frameworks/react-vite/tsconfig.json b/code/frameworks/react-vite/tsconfig.json
new file mode 100644
index 000000000000..534e4ddd108a
--- /dev/null
+++ b/code/frameworks/react-vite/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "types": ["node"],
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**/*.test.*", "src/**/__testfixtures__/**"]
+}
diff --git a/code/frameworks/vue3-vite/README.md b/code/frameworks/vue3-vite/README.md
new file mode 100644
index 000000000000..e8a35450aec9
--- /dev/null
+++ b/code/frameworks/vue3-vite/README.md
@@ -0,0 +1 @@
+# Storybook for React
diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json
new file mode 100644
index 000000000000..f20e40676935
--- /dev/null
+++ b/code/frameworks/vue3-vite/package.json
@@ -0,0 +1,100 @@
+{
+ "name": "@storybook/vue3-vite",
+ "version": "7.0.0-alpha.23",
+ "description": "Storybook for Vue3: Develop Vue3 Component in isolation with Hot Reloading.",
+ "keywords": [
+ "storybook"
+ ],
+ "homepage": "https://github.com/storybookjs/storybook/tree/main/frameworks/vue3-vite",
+ "bugs": {
+ "url": "https://github.com/storybookjs/storybook/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybookjs/storybook.git",
+ "directory": "frameworks/vue3-vite"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "license": "MIT",
+ "exports": {
+ ".": {
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs",
+ "types": "./dist/index.d.ts"
+ },
+ "./preset": {
+ "require": "./dist/preset.js",
+ "import": "./dist/preset.mjs",
+ "types": "./dist/preset.d.ts"
+ },
+ "./package.json": {
+ "require": "./package.json",
+ "import": "./package.json",
+ "types": "./package.json"
+ }
+ },
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist/**/*",
+ "types/**/*",
+ "README.md",
+ "*.js",
+ "*.d.ts"
+ ],
+ "scripts": {
+ "check": "tsc --noEmit",
+ "prepare": "../../../scripts/prepare/bundle.ts"
+ },
+ "dependencies": {
+ "@rollup/pluginutils": "^4.2.0",
+ "@storybook/builder-vite": "7.0.0-alpha.23",
+ "@storybook/core-server": "7.0.0-alpha.23",
+ "@storybook/core-vite": "7.0.0-alpha.23",
+ "@storybook/vue3": "7.0.0-alpha.23",
+ "@types/node": "^14.14.20 || ^16.0.0",
+ "@vitejs/plugin-vue": "^3.0.3",
+ "ast-types": "^0.14.2",
+ "core-js": "^3.8.2",
+ "magic-string": "^0.26.1",
+ "react-docgen": "6.0.0-alpha.1",
+ "regenerator-runtime": "^0.13.7",
+ "vite": "3",
+ "vue-docgen-api": "^4.40.0"
+ },
+ "devDependencies": {
+ "jest-specific-snapshot": "^4.0.0",
+ "typescript": "~4.6.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.5",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bundler": {
+ "entries": [
+ "./src/index.ts",
+ "./src/preset.ts"
+ ],
+ "platform": "node"
+ },
+ "gitHead": "55247a8e36da7061bfced80c588a539d3fda3f04"
+}
diff --git a/code/frameworks/vue3-vite/preset.js b/code/frameworks/vue3-vite/preset.js
new file mode 100644
index 000000000000..2e3d77f1dea6
--- /dev/null
+++ b/code/frameworks/vue3-vite/preset.js
@@ -0,0 +1,2 @@
+console.log('vue3-vite preset!')
+module.exports = require('./dist/preset');
diff --git a/code/frameworks/vue3-vite/src/index.ts b/code/frameworks/vue3-vite/src/index.ts
new file mode 100644
index 000000000000..cf9a0c07e407
--- /dev/null
+++ b/code/frameworks/vue3-vite/src/index.ts
@@ -0,0 +1 @@
+export * from '@storybook/vue3';
diff --git a/code/frameworks/vue3-vite/src/plugins/docgen-handlers/actualNameHandler.ts b/code/frameworks/vue3-vite/src/plugins/docgen-handlers/actualNameHandler.ts
new file mode 100644
index 000000000000..4849df62d087
--- /dev/null
+++ b/code/frameworks/vue3-vite/src/plugins/docgen-handlers/actualNameHandler.ts
@@ -0,0 +1,48 @@
+/**
+ * This is heavily based on the react-docgen `displayNameHandler`
+ * (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
+ * but instead defines an `actualName` property on the generated docs that is taken first from the component's actual name.
+ * This addresses an issue where the name that the generated docs are stored under is incorrectly named with the `displayName`
+ * and not the component's actual name.
+ *
+ * This is inspired by `actualNameHandler` from https://github.com/storybookjs/babel-plugin-react-docgen, but is modified
+ * directly from displayNameHandler, using the same approach as babel-plugin-react-docgen.
+ */
+
+import { namedTypes as t } from 'ast-types';
+import type { NodePath } from 'ast-types/lib/node-path';
+import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/lib/utils';
+import type { Importer } from 'react-docgen/lib/parse';
+import type Documentation from 'react-docgen/lib/Documentation';
+
+export default function actualNameHandler(
+ documentation: Documentation,
+ path: NodePath,
+ importer: Importer
+): void {
+ if (t.ClassDeclaration.check(path.node) || t.FunctionDeclaration.check(path.node)) {
+ documentation.set('actualName', getNameOrValue(path.get('id')));
+ } else if (
+ t.ArrowFunctionExpression.check(path.node) ||
+ t.FunctionExpression.check(path.node) ||
+ isReactForwardRefCall(path, importer)
+ ) {
+ let currentPath = path;
+ while (currentPath.parent) {
+ if (t.VariableDeclarator.check(currentPath.parent.node)) {
+ documentation.set('actualName', getNameOrValue(currentPath.parent.get('id')));
+ return;
+ }
+ if (t.AssignmentExpression.check(currentPath.parent.node)) {
+ const leftPath = currentPath.parent.get('left');
+ if (t.Identifier.check(leftPath.node) || t.Literal.check(leftPath.node)) {
+ documentation.set('actualName', getNameOrValue(leftPath));
+ return;
+ }
+ }
+ currentPath = currentPath.parent;
+ }
+ // Could not find an actual name
+ documentation.set('actualName', '');
+ }
+}
diff --git a/code/frameworks/vue3-vite/src/plugins/vue-docgen.ts b/code/frameworks/vue3-vite/src/plugins/vue-docgen.ts
new file mode 100644
index 000000000000..066b44df134c
--- /dev/null
+++ b/code/frameworks/vue3-vite/src/plugins/vue-docgen.ts
@@ -0,0 +1,27 @@
+import { parse } from 'vue-docgen-api';
+import type { Plugin } from 'vite';
+import { createFilter } from 'vite';
+import MagicString from 'magic-string';
+
+export function vueDocgen(): Plugin {
+ const include = /\.(vue)$/;
+ const filter = createFilter(include);
+
+ return {
+ name: 'vue-docgen',
+
+ async transform(src: string, id: string) {
+ if (!filter(id)) return undefined;
+
+ const metaData = await parse(id);
+ const metaSource = JSON.stringify(metaData);
+ const s = new MagicString(src);
+ s.append(`;_sfc_main.__docgenInfo = ${metaSource}`);
+
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ },
+ };
+}
diff --git a/code/frameworks/vue3-vite/src/preset.ts b/code/frameworks/vue3-vite/src/preset.ts
new file mode 100644
index 000000000000..f97ea61d0afe
--- /dev/null
+++ b/code/frameworks/vue3-vite/src/preset.ts
@@ -0,0 +1,53 @@
+import path from 'path';
+import fs from 'fs';
+import type { StorybookConfig, TypescriptOptions } from '@storybook/core-vite';
+
+export const addons: StorybookConfig['addons'] = ['@storybook/vue3'];
+
+export const core: StorybookConfig['core'] = {
+ builder: '@storybook/builder-vite',
+};
+
+export function readPackageJson(): Record | false {
+ const packageJsonPath = path.resolve('package.json');
+ if (!fs.existsSync(packageJsonPath)) {
+ return false;
+ }
+
+ const jsonContent = fs.readFileSync(packageJsonPath, 'utf8');
+ return JSON.parse(jsonContent);
+}
+
+export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => {
+ const { plugins = [] } = config;
+
+ try {
+ // eslint-disable-next-line global-require
+ const vuePlugin = require('@vitejs/plugin-vue');
+ plugins.push(vuePlugin());
+ const { vueDocgen } = await import('./plugins/vue-docgen');
+ plugins.push(vueDocgen());
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') {
+ throw new Error(
+ '@storybook/builder-vite requires @vitejs/plugin-vue to be installed ' +
+ 'when using @storybook/vue or @storybook/vue3.' +
+ ' Please install it and start storybook again.'
+ );
+ }
+ throw err;
+ }
+
+ const updated = {
+ ...config,
+ plugins,
+ resolve: {
+ ...config?.resolve,
+ alias: {
+ ...config?.resolve?.alias,
+ vue: 'vue/dist/vue.esm-bundler.js',
+ },
+ },
+ };
+ return updated;
+};
diff --git a/code/frameworks/vue3-vite/src/typings.d.ts b/code/frameworks/vue3-vite/src/typings.d.ts
new file mode 100644
index 000000000000..f21e29cca314
--- /dev/null
+++ b/code/frameworks/vue3-vite/src/typings.d.ts
@@ -0,0 +1,46 @@
+declare module '@storybook/semver';
+declare module 'global';
+
+// TODO: Replace, as soon as @types/react-dom 17.0.14 is used
+// Source: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fb0f14b7a35cde26ffaa82e7536c062e593e9ae6/types/react-dom/client.d.ts
+declare module 'react-dom/client' {
+ import React = require('react');
+ export interface HydrationOptions {
+ onHydrated?(suspenseInstance: Comment): void;
+ onDeleted?(suspenseInstance: Comment): void;
+ /**
+ * Prefix for `useId`.
+ */
+ identifierPrefix?: string;
+ onRecoverableError?: (error: unknown) => void;
+ }
+
+ export interface RootOptions {
+ /**
+ * Prefix for `useId`.
+ */
+ identifierPrefix?: string;
+ onRecoverableError?: (error: unknown) => void;
+ }
+
+ export interface Root {
+ render(children: React.ReactChild | Iterable): void;
+ unmount(): void;
+ }
+
+ /**
+ * Replaces `ReactDOM.render` when the `.render` method is called and enables Concurrent Mode.
+ *
+ * @see https://reactjs.org/docs/concurrent-mode-reference.html#createroot
+ */
+ export function createRoot(
+ container: Element | Document | DocumentFragment | Comment,
+ options?: RootOptions
+ ): Root;
+
+ export function hydrateRoot(
+ container: Element | Document | DocumentFragment | Comment,
+ initialChildren: React.ReactChild | Iterable,
+ options?: HydrationOptions
+ ): Root;
+}
diff --git a/code/frameworks/vue3-vite/tsconfig.json b/code/frameworks/vue3-vite/tsconfig.json
new file mode 100644
index 000000000000..534e4ddd108a
--- /dev/null
+++ b/code/frameworks/vue3-vite/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "types": ["node"],
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**/*.test.*", "src/**/__testfixtures__/**"]
+}
diff --git a/code/lib/builder-vite/README.md b/code/lib/builder-vite/README.md
new file mode 100644
index 000000000000..88d077113f2d
--- /dev/null
+++ b/code/lib/builder-vite/README.md
@@ -0,0 +1,225 @@
+# Storybook builder for Vite
+
+Build your stories with [vite](https://vitejs.dev/) for fast startup times and near-instant HMR.
+
+# Table of Contents
+
+- [Migration from storybook-builder-vite](#migration-from-storybook-builder-vite)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Getting started with Vite and Storybook (on a new project)](#getting-started-with-vite-and-storybook-on-a-new-project)
+ - [Migration from webpack / CRA](#migration-from-webpack--cra)
+ - [Customize Vite config](#customize-vite-config)
+ - [Svelte Customization](#svelte-customization)
+ - [TypeScript](#typescript)
+ - [React Docgen](#react-docgen)
+ - [Note about working directory](#note-about-working-directory)
+- [Known issues](#known-issues)
+- [Contributing](#contributing)
+ - [About this codebase](#about-this-codebase)
+
+## Migration from storybook-builder-vite
+
+This project has moved from `storybook-builder-vite` to `@storybook/builder-vite` as part of a larger effort to improve Vite support in Storybook. To automatically migrate your existing project, you can run
+
+```bash
+npx sb@next automigrate
+```
+
+To manually migrate:
+
+1. Remove `storybook-builder-vite` from your `package.json` dependencies
+2. Install `@storybook/builder-vite`
+3. Update your `core.builder` setting in `.storybook/main.js` to `@storybook/builder-vite`.
+
+## Installation
+
+Requirements:
+
+- Vite 3.0 or newer (for Vite v2 (2.5+), use `@storybook/builder-vite@0.1.x`)
+- Storybook 6.4.0 or newer (for storybook 6.3, use `storybook-builder-vite@0.1.16`)
+
+```bash
+npm install @storybook/builder-vite --save-dev
+```
+
+or
+
+```bash
+yarn add --dev @storybook/builder-vite
+```
+
+or
+
+```bash
+pnpm add --save-dev @storybook/builder-vite
+```
+
+Note: when using `pnpm`, you may need to enable [shamefully-hoist](https://pnpm.io/npmrc#shamefully-hoist), until https://github.com/storybookjs/builder-vite/issues/55 can be fixed.
+
+## Usage
+
+In your `main.js` configuration file,
+set `core: { builder: "@storybook/builder-vite" }`.
+
+> For autoreload of react stories to work, they need to have a `.stories.tsx` or `.stories.jsx` file suffix.
+> See also [#53](https://github.com/storybookjs/builder-vite/pull/53)
+
+The builder supports both development mode in Storybook, and building a static production version.
+
+### Getting started with Vite and Storybook (on a new project)
+
+See https://vitejs.dev/guide/#scaffolding-your-first-vite-project,
+
+```
+npm create vite@latest # follow the prompts
+npx sb init --builder @storybook/builder-vite && npm run storybook
+```
+
+### Migration from webpack / CRA
+
+1. Install `vite` and `@storybook/builder-vite`
+2. Remove any explicit project dependencies on `webpack`, `react-scripts`, and any other webpack plugins or loaders.
+3. If you were previously using `@storybook/manager-webpack5`, you'll need to remove it, since currently the vite builder only works with `manager-webpack4`, which is the default and does not need to be installed manually. Also remove `@storybook/builder-webpack5` or `@storybook/builder-webpack4` if they are installed.
+4. Set `core: { builder: "@storybook/builder-vite" }` in your `.storybook/main.js` file.
+5. Remove storybook webpack cache (`rm -rf node_modules/.cache`)
+6. Update your `/public/index.html` file for vite (be sure there are no `%PUBLIC_URL%` inside it, which is a CRA variable)
+7. Be sure that any files containing JSX syntax use a `.jsx` or `.tsx` file extension, which [vite requires](https://vitejs.dev/guide/features.html#jsx). This includes `.storybook/preview.jsx` if it contains JSX syntax.
+8. If you are using `@storybook/addon-interactions`, for now you'll need to add a [workaround](https://github.com/storybookjs/storybook/issues/18399) for jest-mock relying on the node `global` variable by creating a `.storybook/preview-head.html` file containing the following:
+
+```html
+
+```
+
+9. Start up your storybook using the same `yarn storybook` or `npm run storybook` commands you are used to.
+
+For other details about the differences between vite and webpack projects, be sure to read through the [vite documentation](https://vitejs.dev/).
+
+### Customize Vite config
+
+The builder will _not_ read your `vite.config.js` file by default.
+
+In `.storybook/main.js` (or whatever your Storybook config file is named)
+you can override the Vite config:
+
+```javascript
+// use `mergeConfig` to recursively merge Vite options
+const { mergeConfig } = require('vite');
+
+module.exports = {
+ async viteFinal(config, { configType }) {
+ // return the customized config
+ return mergeConfig(config, {
+ // customize the Vite config here
+ resolve: {
+ alias: { foo: 'bar' },
+ },
+ });
+ },
+ // ... other options here
+};
+```
+
+The `viteFinal` function will give you `config` which is
+the builder's own Vite config. You can tweak this as you want,
+for example to set up aliases, add new plugins etc.
+
+The `configType` variable will be either `"DEVELOPMENT"` or `"PRODUCTION"`.
+
+The function should return the updated Vite configuration.
+
+### Svelte Customization
+
+When using this builder with Svelte, your `.storybook/main.js` (or equivalent)
+can contain a `svelteOptions` object to pass custom options to
+[`vite-plugin-svelte`](https://github.com/sveltejs/vite-plugin-svelte/tree/main/packages/vite-plugin-svelte):
+
+```javascript
+const preprocess = require('svelte-preprocess');
+
+module.exports = {
+ svelteOptions: {
+ preprocess: preprocess({
+ typescript: true,
+ postcss: true,
+ sourceMap: true,
+ }),
+ },
+};
+```
+
+### TypeScript
+
+Configure your `.storybook/main.ts` to use TypeScript:
+
+```typescript
+import type { StorybookViteConfig } from '@storybook/builder-vite';
+
+const config: StorybookViteConfig = {
+ // other storybook options...,
+ async viteFinal(config, options) {
+ // modify and return config
+ },
+};
+
+export default config;
+```
+
+Or alternatively, you can use named exports:
+
+```typescript
+import type { ViteFinal } from '@storybook/builder-vite';
+
+export const viteFinal: ViteFinal = async (config, options) => {
+ // modify and return config
+};
+```
+
+See [Customize Vite config](#customize-vite-config) for details about using `viteFinal`.
+
+### React Docgen
+
+Docgen is used in Storybook to populate the props table in docs view, the controls panel, and for several other addons. Docgen is supported in vue and react, and there are two docgen options when using react, `react-docgen` and `react-docgen-typescript`. You can learn more about the pros/cons of each in [this gist](https://gist.github.com/shilman/036313ffa3af52ca986b375d90ea46b0). By default, if we find a `typescript` dependency in your `package.json` file, we will assume you're using typescript and will choose `react-docgen-typescript`. You can change this by setting the `typescript.reactDocgen` option in your `.storybook/main.js` file:
+
+```javascript
+module.exports = {
+ typescript: {
+ reactDocgen: 'react-docgen`
+ }
+}
+```
+
+If you're using TypeScript, we encourage you to experiment and see which option works better for your project.
+
+### Note about working directory
+
+The builder will by default enable Vite's [server.fs.strict](https://vitejs.dev/config/#server-fs-strict)
+option, for increased security. The default project `root` is set to the parent directory of the
+storybook configuration directory. This can be overridden in viteFinal.
+
+## Known issues
+
+- HMR: saving a story file does not hot-module-reload, a full reload happens instead. HMR works correctly when saving component files.
+
+## Contributing
+
+The Vite builder cannot build itself.
+Are you willing to contribute? We are especially looking for vue and svelte experts, as the current maintainers are react users.
+
+https://github.com/storybookjs/builder-vite/issues/11
+
+Have a look at the GitHub issues for known bugs. If you find any new bugs,
+feel free to create an issue or send a pull request!
+
+Please read the [How to contribute](/CONTRIBUTING.md) guide.
+
+### About this codebase
+
+The code is a monorepo with the core `@storybook/builder-vite` package,
+and examples (like `examples/react`) to test the builder implementation.
+
+Similar to the main storybook monorepo, you need yarn to develop this builder, because the project is organized as yarn workspaces.
+This lets you write new code in the core builder package, and instantly use them from
+the example packages.
diff --git a/code/lib/builder-vite/input/iframe.html b/code/lib/builder-vite/input/iframe.html
new file mode 100644
index 000000000000..bcabc10ead11
--- /dev/null
+++ b/code/lib/builder-vite/input/iframe.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/code/lib/builder-vite/input/react-dom-client-placeholder.js b/code/lib/builder-vite/input/react-dom-client-placeholder.js
new file mode 100644
index 000000000000..ac58bff7de38
--- /dev/null
+++ b/code/lib/builder-vite/input/react-dom-client-placeholder.js
@@ -0,0 +1,3 @@
+// This file is to work around https://github.com/vitejs/vite/issues/6007
+// For react < 18 projects, where `react-dom/client` does not exist, yet is
+// conditionally imported by @storybook/react.
diff --git a/code/lib/builder-vite/package.json b/code/lib/builder-vite/package.json
new file mode 100644
index 000000000000..8433e602e228
--- /dev/null
+++ b/code/lib/builder-vite/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "@storybook/builder-vite",
+ "version": "7.0.0-alpha.23",
+ "description": "A plugin to run and build Storybooks with Vite",
+ "homepage": "https://github.com/storybookjs/storybook/tree/main/code/lib/builder-vite/#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybookjs/storybook.git",
+ "directory": "code/lib/builder-vite"
+ },
+ "license": "MIT",
+ "author": "Eirik Sletteberg",
+ "main": "dist/cjs/index.js",
+ "module": "dist/esm/index.js",
+ "types": "dist/types/index.d.ts",
+ "scripts": {
+ "check": "tsc --noEmit",
+ "prepare": "node ../../../scripts/prepare.js"
+ },
+ "dependencies": {
+ "@joshwooding/vite-plugin-react-docgen-typescript": "0.0.5",
+ "@storybook/addons": "7.0.0-alpha.23",
+ "@storybook/channel-postmessage": "7.0.0-alpha.23",
+ "@storybook/channel-websocket": "7.0.0-alpha.23",
+ "@storybook/client-api": "7.0.0-alpha.23",
+ "@storybook/client-logger": "7.0.0-alpha.23",
+ "@storybook/core-common": "7.0.0-alpha.23",
+ "@storybook/core-vite": "7.0.0-alpha.23",
+ "@storybook/mdx1-csf": "0.0.5-canary.13.9ce928a.0",
+ "@storybook/node-logger": "7.0.0-alpha.23",
+ "@storybook/preview-web": "7.0.0-alpha.23",
+ "@storybook/source-loader": "7.0.0-alpha.23",
+ "@vitejs/plugin-react": "^2.0.0",
+ "ast-types": "^0.14.2",
+ "es-module-lexer": "^0.9.3",
+ "glob": "^7.2.0",
+ "glob-promise": "^4.2.0",
+ "magic-string": "^0.26.1",
+ "react-docgen": "^6.0.0-alpha.0",
+ "slash": "^3.0.0",
+ "sveltedoc-parser": "^4.2.1",
+ "vite": "3"
+ },
+ "devDependencies": {
+ "@storybook/mdx2-csf": "^0.0.3",
+ "@sveltejs/vite-plugin-svelte": "^1.0.0",
+ "@types/express": "^4.17.13",
+ "@types/node": "^17.0.23",
+ "svelte": "^3.49.0",
+ "typescript": "~4.6.3"
+ },
+ "peerDependencies": {
+ "@storybook/mdx2-csf": "^0.0.3",
+ "svelte": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@storybook/mdx2-csf": {
+ "optional": true
+ },
+ "svelte": {
+ "optional": true
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "gitHead": "74bdb53f67dd59bae2661c668d2d5d4296113281"
+}
diff --git a/code/lib/builder-vite/src/build.ts b/code/lib/builder-vite/src/build.ts
new file mode 100644
index 000000000000..d0a36401fc7f
--- /dev/null
+++ b/code/lib/builder-vite/src/build.ts
@@ -0,0 +1,32 @@
+import { build as viteBuild } from 'vite';
+import { stringifyProcessEnvs } from './envs';
+import { commonConfig } from './vite-config';
+
+import type { EnvsRaw, ExtendedOptions } from './types';
+
+export async function build(options: ExtendedOptions) {
+ const { presets } = options;
+
+ const baseConfig = await commonConfig(options, 'build');
+ const config = {
+ ...baseConfig,
+ build: {
+ outDir: options.outputDir,
+ emptyOutDir: false, // do not clean before running Vite build - Storybook has already added assets in there!
+ sourcemap: true,
+ },
+ };
+
+ const finalConfig = await presets.apply('viteFinal', config, options);
+
+ const envsRaw = await presets.apply>('env');
+ // Stringify env variables after getting `envPrefix` from the final config
+ const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix);
+ // Update `define`
+ finalConfig.define = {
+ ...finalConfig.define,
+ ...envs,
+ };
+
+ await viteBuild(finalConfig);
+}
diff --git a/code/lib/builder-vite/src/code-generator-plugin.ts b/code/lib/builder-vite/src/code-generator-plugin.ts
new file mode 100644
index 000000000000..e27240673b5e
--- /dev/null
+++ b/code/lib/builder-vite/src/code-generator-plugin.ts
@@ -0,0 +1,149 @@
+/* eslint-disable no-param-reassign */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import { mergeConfig } from 'vite';
+import type { Plugin } from 'vite';
+import { transformIframeHtml } from './transform-iframe-html';
+import { generateIframeScriptCode } from './codegen-iframe-script';
+import { generateModernIframeScriptCode } from './codegen-modern-iframe-script';
+import { generateImportFnScriptCode } from './codegen-importfn-script';
+import { generateVirtualStoryEntryCode, generatePreviewEntryCode } from './codegen-entries';
+import { generateAddonSetupCode } from './codegen-set-addon-channel';
+
+import type { ExtendedOptions } from './types';
+
+import {
+ virtualAddonSetupFile,
+ virtualFileId,
+ virtualPreviewFile,
+ virtualStoriesFile,
+} from './virtual-file-names';
+
+export function codeGeneratorPlugin(options: ExtendedOptions): Plugin {
+ const iframePath = path.resolve(__dirname, '../..', 'input', 'iframe.html');
+ let iframeId: string;
+
+ // noinspection JSUnusedGlobalSymbols
+ return {
+ name: 'storybook-vite-code-generator-plugin',
+ enforce: 'pre',
+ configureServer(server) {
+ // invalidate the whole vite-app.js script on every file change.
+ // (this might be a little too aggressive?)
+ server.watcher.on('change', () => {
+ const appModule = server.moduleGraph.getModuleById(virtualFileId);
+ if (appModule) {
+ server.moduleGraph.invalidateModule(appModule);
+ }
+ const storiesModule = server.moduleGraph.getModuleById(virtualStoriesFile);
+ if (storiesModule) {
+ server.moduleGraph.invalidateModule(storiesModule);
+ }
+ });
+
+ // Adding new story files is not covered by the change event above. So we need to detect this and trigger
+ // HMR to update the importFn.
+ server.watcher.on('add', (path) => {
+ // TODO maybe use the stories declaration in main
+ if (/\.stories\.([tj])sx?$/.test(path) || /\.(story|stories).mdx$/.test(path)) {
+ // We need to emit a change event to trigger HMR
+ server.watcher.emit('change', virtualStoriesFile);
+ }
+ });
+ },
+ config(config, { command }) {
+ // If we are building the static distribution, add iframe.html as an entry.
+ // In development mode, it's not an entry - instead, we use an express middleware
+ // to serve iframe.html. The reason is that Vite's dev server (at the time of writing)
+ // does not support virtual files as entry points.
+ if (command === 'build') {
+ if (!config.build) {
+ config.build = {};
+ }
+ config.build.rollupOptions = {
+ ...config.build.rollupOptions,
+ input: iframePath,
+ };
+ }
+
+ // Detect if react 18 is installed. If not, alias it to a virtual placeholder file.
+ try {
+ require.resolve('react-dom/client', { paths: [config.root || process.cwd()] });
+ } catch (e) {
+ if (isNodeError(e) && e.code === 'MODULE_NOT_FOUND') {
+ config.resolve = mergeConfig(config.resolve ?? {}, {
+ alias: {
+ 'react-dom/client': path.resolve(
+ __dirname,
+ '../..',
+ 'input',
+ 'react-dom-client-placeholder.js'
+ ),
+ },
+ });
+ }
+ }
+ },
+ configResolved(config) {
+ iframeId = `${config.root}/iframe.html`;
+ },
+ resolveId(source) {
+ if (source === virtualFileId) {
+ return virtualFileId;
+ }
+ if (source === iframePath) {
+ return iframeId;
+ }
+ if (source === virtualStoriesFile) {
+ return virtualStoriesFile;
+ }
+ if (source === virtualPreviewFile) {
+ return virtualPreviewFile;
+ }
+ if (source === virtualAddonSetupFile) {
+ return virtualAddonSetupFile;
+ }
+ return undefined;
+ },
+ async load(id) {
+ const storyStoreV7 = options.features?.storyStoreV7;
+ if (id === virtualStoriesFile) {
+ if (storyStoreV7) {
+ return generateImportFnScriptCode(options);
+ }
+ return generateVirtualStoryEntryCode(options);
+ }
+
+ if (id === virtualAddonSetupFile) {
+ return generateAddonSetupCode();
+ }
+
+ if (id === virtualPreviewFile && !storyStoreV7) {
+ return generatePreviewEntryCode(options);
+ }
+
+ if (id === virtualFileId) {
+ if (storyStoreV7) {
+ return generateModernIframeScriptCode(options);
+ }
+ return generateIframeScriptCode(options);
+ }
+
+ if (id === iframeId) {
+ return fs.readFileSync(path.resolve(__dirname, '../..', 'input', 'iframe.html'), 'utf-8');
+ }
+
+ return undefined;
+ },
+ async transformIndexHtml(html, ctx) {
+ if (ctx.path !== '/iframe.html') {
+ return undefined;
+ }
+ return transformIframeHtml(html, options);
+ },
+ };
+}
+
+// Refines an error received from 'catch' to be a NodeJS exception
+const isNodeError = (error: unknown): error is NodeJS.ErrnoException => error instanceof Error;
diff --git a/code/lib/builder-vite/src/codegen-entries.ts b/code/lib/builder-vite/src/codegen-entries.ts
new file mode 100644
index 000000000000..7cdbb0eb4c8d
--- /dev/null
+++ b/code/lib/builder-vite/src/codegen-entries.ts
@@ -0,0 +1,43 @@
+import { loadPreviewOrConfigFile } from '@storybook/core-common';
+import type { Options } from '@storybook/core-common';
+import slash from 'slash';
+import { normalizePath } from 'vite';
+import type { ExtendedOptions } from './types';
+import { listStories } from './list-stories';
+
+const absoluteFilesToImport = (files: string[], name: string) =>
+ files.map((el, i) => `import ${name ? `* as ${name}_${i} from ` : ''}'/@fs/${normalizePath(el)}'`).join('\n');
+
+export async function generateVirtualStoryEntryCode(options: ExtendedOptions) {
+ const storyEntries = await listStories(options);
+ const resolveMap = storyEntries.reduce>(
+ (prev, entry) => ({ ...prev, [entry]: entry.replace(slash(process.cwd()), '.') }),
+ {}
+ );
+ const modules = storyEntries.map((entry, i) => `${JSON.stringify(entry)}: story_${i}`).join(',');
+
+ return `
+ ${absoluteFilesToImport(storyEntries, 'story')}
+
+ function loadable(key) {
+ return {${modules}}[key];
+ }
+
+ Object.assign(loadable, {
+ keys: () => (${JSON.stringify(Object.keys(resolveMap))}),
+ resolve: (key) => (${JSON.stringify(resolveMap)}[key])
+ });
+
+ export function configStories(configure) {
+ configure(loadable, { hot: import.meta.hot }, false);
+ }
+ `.trim();
+}
+
+export async function generatePreviewEntryCode({ configDir }: Options) {
+ const previewFile = loadPreviewOrConfigFile({ configDir });
+ if (!previewFile) return '';
+
+ return `import * as preview from '${slash(previewFile)}';
+ export default preview;`;
+}
diff --git a/code/lib/builder-vite/src/codegen-iframe-script.ts b/code/lib/builder-vite/src/codegen-iframe-script.ts
new file mode 100644
index 000000000000..7e9dd7047bce
--- /dev/null
+++ b/code/lib/builder-vite/src/codegen-iframe-script.ts
@@ -0,0 +1,115 @@
+import { virtualPreviewFile, virtualStoriesFile } from './virtual-file-names';
+import { transformAbsPath } from './utils/transform-abs-path';
+import type { ExtendedOptions } from './types';
+
+export async function generateIframeScriptCode(options: ExtendedOptions) {
+ const { presets, frameworkPath, framework } = options;
+ const frameworkImportPath = frameworkPath || `@storybook/${framework}`;
+
+ const presetEntries = await presets.apply('config', [], options);
+ const configEntries = [...presetEntries].filter(Boolean);
+
+ const absoluteFilesToImport = (files: string[], name: string) =>
+ files
+ .map((el, i) => `import ${name ? `* as ${name}_${i} from ` : ''}'${transformAbsPath(el)}'`)
+ .join('\n');
+
+ const importArray = (name: string, length: number) =>
+ new Array(length).fill(0).map((_, i) => `${name}_${i}`);
+
+ // noinspection UnnecessaryLocalVariableJS
+ /** @todo Inline variable and remove `noinspection` */
+ // language=JavaScript
+ const code = `
+ // Ensure that the client API is initialized by the framework before any other iframe code
+ // is loaded. That way our client-apis can assume the existence of the API+store
+ import { configure } from '${frameworkImportPath}';
+
+ import * as clientApi from "@storybook/client-api";
+ import { logger } from '@storybook/client-logger';
+ ${absoluteFilesToImport(configEntries, 'config')}
+ import * as preview from '${virtualPreviewFile}';
+ import { configStories } from '${virtualStoriesFile}';
+
+ const {
+ addDecorator,
+ addParameters,
+ addLoader,
+ addArgTypesEnhancer,
+ addArgsEnhancer,
+ setGlobalRender,
+ } = clientApi;
+
+ const configs = [${importArray('config', configEntries.length)
+ .concat('preview.default')
+ .join(',')}].filter(Boolean)
+
+ configs.forEach(config => {
+ Object.keys(config).forEach((key) => {
+ const value = config[key];
+ switch (key) {
+ case 'args': {
+ if (typeof clientApi.addArgs !== "undefined") {
+ return clientApi.addArgs(value);
+ } else {
+ return logger.warn(
+ "Could not add global args. Please open an issue in storybookjs/builder-vite."
+ );
+ }
+ }
+ case 'argTypes': {
+ if (typeof clientApi.addArgTypes !== "undefined") {
+ return clientApi.addArgTypes(value);
+ } else {
+ return logger.warn(
+ "Could not add global argTypes. Please open an issue in storybookjs/builder-vite."
+ );
+ }
+ }
+ case 'decorators': {
+ return value.forEach((decorator) => addDecorator(decorator, false));
+ }
+ case 'loaders': {
+ return value.forEach((loader) => addLoader(loader, false));
+ }
+ case 'parameters': {
+ return addParameters({ ...value }, false);
+ }
+ case 'argTypesEnhancers': {
+ return value.forEach((enhancer) => addArgTypesEnhancer(enhancer));
+ }
+ case 'argsEnhancers': {
+ return value.forEach((enhancer) => addArgsEnhancer(enhancer))
+ }
+ case 'render': {
+ return setGlobalRender(value)
+ }
+ case 'globals':
+ case 'globalTypes': {
+ const v = {};
+ v[key] = value;
+ return addParameters(v, false);
+ }
+ case 'decorateStory':
+ case 'applyDecorators':
+ case 'renderToDOM': {
+ return null; // This key is not handled directly in v6 mode.
+ }
+ default: {
+ // eslint-disable-next-line prefer-template
+ return console.log(key + ' was not supported :( !');
+ }
+ }
+ });
+ })
+
+ /* TODO: not quite sure what to do with this, to fix HMR
+ if (import.meta.hot) {
+ import.meta.hot.accept();
+ }
+ */
+
+ configStories(configure);
+ `.trim();
+ return code;
+}
diff --git a/code/lib/builder-vite/src/codegen-importfn-script.ts b/code/lib/builder-vite/src/codegen-importfn-script.ts
new file mode 100644
index 000000000000..958ca5c59efd
--- /dev/null
+++ b/code/lib/builder-vite/src/codegen-importfn-script.ts
@@ -0,0 +1,56 @@
+import * as path from 'path';
+import { normalizePath } from 'vite';
+import type { Options } from '@storybook/core-common';
+import { logger } from '@storybook/node-logger';
+
+import { listStories } from './list-stories';
+
+/**
+ * This file is largely based on https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/lib/core-common/src/utils/to-importFn.ts
+ */
+
+/**
+ * Paths get passed either with no leading './' - e.g. `src/Foo.stories.js`,
+ * or with a leading `../` (etc), e.g. `../src/Foo.stories.js`.
+ * We want to deal in importPaths relative to the working dir, so we normalize
+ */
+function toImportPath(relativePath: string) {
+ return relativePath.startsWith('../') ? relativePath : `./${relativePath}`;
+}
+
+/**
+ * This function takes an array of stories and creates a mapping between the stories' relative paths
+ * to the working directory and their dynamic imports. The import is done in an asynchronous function
+ * to delay loading. It then creates a function, `importFn(path)`, which resolves a path to an import
+ * function and this is called by Storybook to fetch a story dynamically when needed.
+ * @param stories An array of absolute story paths.
+ */
+async function toImportFn(stories: string[]) {
+ const objectEntries = stories.map((file) => {
+ const ext = path.extname(file);
+ const relativePath = normalizePath(path.relative(process.cwd(), file));
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mdx'].includes(ext)) {
+ logger.warn(`Cannot process ${ext} file with storyStoreV7: ${relativePath}`);
+ }
+
+ return ` '${toImportPath(relativePath)}': async () => import('/@fs/${file}')`;
+ });
+
+ return `
+ const importers = {
+ ${objectEntries.join(',\n')}
+ };
+
+ export async function importFn(path) {
+ return importers[path]();
+ }
+ `;
+}
+
+export async function generateImportFnScriptCode(options: Options) {
+ // First we need to get an array of stories and their absolute paths.
+ const stories = await listStories(options);
+
+ // We can then call toImportFn to create a function that can be used to load each story dynamically.
+ return (await toImportFn(stories)).trim();
+}
diff --git a/code/lib/builder-vite/src/codegen-modern-iframe-script.ts b/code/lib/builder-vite/src/codegen-modern-iframe-script.ts
new file mode 100644
index 000000000000..a223c52fc2f4
--- /dev/null
+++ b/code/lib/builder-vite/src/codegen-modern-iframe-script.ts
@@ -0,0 +1,70 @@
+import { loadPreviewOrConfigFile } from '@storybook/core-common';
+import { virtualStoriesFile, virtualAddonSetupFile } from './virtual-file-names';
+import { transformAbsPath } from './utils/transform-abs-path';
+import type { ExtendedOptions } from './types';
+
+export async function generateModernIframeScriptCode(options: ExtendedOptions) {
+ const { presets, configDir, framework } = options;
+
+ const previewOrConfigFile = loadPreviewOrConfigFile({ configDir });
+ const presetEntries = await presets.apply('config', [], options);
+ const configEntries = [...presetEntries, previewOrConfigFile]
+ .filter(Boolean)
+ .map((configEntry) => transformAbsPath(configEntry as string));
+
+ const generateHMRHandler = (framework: string): string => {
+ // Web components are not compatible with HMR, so disable HMR, reload page instead.
+ if (framework === 'web-components') {
+ return `
+ if (import.meta.hot) {
+ import.meta.hot.decline();
+ }`.trim();
+ }
+
+ return `
+ if (import.meta.hot) {
+ import.meta.hot.accept('${virtualStoriesFile}', (newModule) => {
+ // importFn has changed so we need to patch the new one in
+ preview.onStoriesChanged({ importFn: newModule.importFn });
+ });
+
+ import.meta.hot.accept(${JSON.stringify(configEntries)}, ([...newConfigEntries]) => {
+ const newGetProjectAnnotations = () => composeConfigs(newConfigEntries);
+
+ // getProjectAnnotations has changed so we need to patch the new one in
+ preview.onGetProjectAnnotationsChanged({ getProjectAnnotations: newGetProjectAnnotations });
+ });
+ }`.trim();
+ };
+
+ /**
+ * This code is largely taken from https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/lib/builder-webpack4/src/preview/virtualModuleModernEntry.js.handlebars
+ * Some small tweaks were made to `getProjectAnnotations` (since `import()` needs to be resolved asynchronously)
+ * and the HMR implementation has been tweaked to work with Vite.
+ * @todo Inline variable and remove `noinspection`
+ */
+ const code = `
+ import { composeConfigs, PreviewWeb } from '@storybook/preview-web';
+ import { ClientApi } from '@storybook/client-api';
+ import '${virtualAddonSetupFile}';
+ import { importFn } from '${virtualStoriesFile}';
+
+ const getProjectAnnotations = async () => {
+ const configs = await Promise.all([${configEntries
+ .map((configEntry) => `import('${configEntry}')`)
+ .join(',\n')}])
+ return composeConfigs(configs);
+ }
+
+ const preview = new PreviewWeb();
+
+ window.__STORYBOOK_PREVIEW__ = preview;
+ window.__STORYBOOK_STORY_STORE__ = preview.storyStore;
+ window.__STORYBOOK_CLIENT_API__ = new ClientApi({ storyStore: preview.storyStore });
+
+ preview.initialize({ importFn, getProjectAnnotations });
+
+ ${generateHMRHandler(framework)};
+ `.trim();
+ return code;
+}
diff --git a/code/lib/builder-vite/src/codegen-set-addon-channel.ts b/code/lib/builder-vite/src/codegen-set-addon-channel.ts
new file mode 100644
index 000000000000..f00e3d83e1e3
--- /dev/null
+++ b/code/lib/builder-vite/src/codegen-set-addon-channel.ts
@@ -0,0 +1,18 @@
+export function generateAddonSetupCode() {
+ return `
+ import { createChannel as createPostMessageChannel } from '@storybook/channel-postmessage';
+ import { createChannel as createWebSocketChannel } from '@storybook/channel-websocket';
+ import { addons } from '@storybook/addons';
+
+ const channel = createPostMessageChannel({ page: 'preview' });
+ addons.setChannel(channel);
+ window.__STORYBOOK_ADDONS_CHANNEL__ = channel;
+
+ const { SERVER_CHANNEL_URL } = globalThis;
+ if (SERVER_CHANNEL_URL) {
+ const serverChannel = createWebSocketChannel({ url: SERVER_CHANNEL_URL });
+ addons.setServerChannel(serverChannel);
+ window.__STORYBOOK_SERVER_CHANNEL__ = serverChannel;
+ }
+ `.trim();
+}
diff --git a/code/lib/builder-vite/src/declarations/extract-stories.d.ts b/code/lib/builder-vite/src/declarations/extract-stories.d.ts
new file mode 100644
index 000000000000..2731f657cc5e
--- /dev/null
+++ b/code/lib/builder-vite/src/declarations/extract-stories.d.ts
@@ -0,0 +1,18 @@
+/**
+ * @see https://github.com/storybookjs/addon-svelte-csf/blob/f72b8f28dabbb99c92e12d0170d3c1db4397ee7c/src/parser/extract-stories.ts
+ */
+declare module '@storybook/addon-svelte-csf/dist/cjs/parser/extract-stories' {
+ interface StoryDef {
+ name: string;
+ template: boolean;
+ source: string;
+ hasArgs: boolean;
+ }
+
+ interface StoriesDef {
+ stories: Record;
+ allocatedIds: string[];
+ }
+
+ function extractStories(component: string): { stories: StoriesDef; allocatedIds: string[] };
+}
diff --git a/code/lib/builder-vite/src/declarations/svetle-stories-loader.d.ts b/code/lib/builder-vite/src/declarations/svetle-stories-loader.d.ts
new file mode 100644
index 000000000000..1ae04708af52
--- /dev/null
+++ b/code/lib/builder-vite/src/declarations/svetle-stories-loader.d.ts
@@ -0,0 +1,7 @@
+/**
+ * @see https://github.com/storybookjs/addon-svelte-csf/blob/f72b8f28dabbb99c92e12d0170d3c1db4397ee7c/src/parser/svelte-stories-loader.ts
+ * @see https://github.com/sveltejs/svelte/blob/deed340cf5d9c278f9a0605297ad6e4a3a1579d9/src/compiler/compile/utils/get_name_from_filename.ts
+ */
+declare module '@storybook/addon-svelte-csf/dist/cjs/parser/svelte-stories-loader' {
+ function getNameFromFilename(filename: string): string;
+}
diff --git a/code/lib/builder-vite/src/envs.ts b/code/lib/builder-vite/src/envs.ts
new file mode 100644
index 000000000000..107391642627
--- /dev/null
+++ b/code/lib/builder-vite/src/envs.ts
@@ -0,0 +1,50 @@
+import { stringifyEnvs } from '@storybook/core-common';
+
+import type { UserConfig } from 'vite';
+import type { EnvsRaw } from './types';
+
+// Allowed env variables on the client
+const allowedEnvVariables = [
+ 'STORYBOOK',
+ // Vite `import.meta.env` default variables
+ // @see https://github.com/vitejs/vite/blob/6b8d94dca2a1a8b4952e3e3fcd0aed1aedb94215/packages/vite/types/importMeta.d.ts#L68-L75
+ 'BASE_URL',
+ 'MODE',
+ 'DEV',
+ 'PROD',
+ 'SSR',
+];
+
+// Env variables starts with env prefix will be exposed to your client source code via `import.meta.env`
+export const allowedEnvPrefix = ['VITE_', 'STORYBOOK_'];
+
+/**
+ * Customized version of stringifyProcessEnvs from @storybook/core-common which
+ * uses import.meta.env instead of process.env and checks for allowed variables.
+ */
+export function stringifyProcessEnvs(raw: EnvsRaw, envPrefix: UserConfig['envPrefix']) {
+ const updatedRaw: EnvsRaw = {};
+ const envs = Object.entries(raw).reduce(
+ (acc: EnvsRaw, [key, value]) => {
+ // Only add allowed values OR values from array OR string started with allowed prefixes
+ if (
+ allowedEnvVariables.includes(key) ||
+ (Array.isArray(envPrefix) && !!envPrefix.find((prefix) => key.startsWith(prefix))) ||
+ (typeof envPrefix === 'string' && key.startsWith(envPrefix))
+ ) {
+ acc[`import.meta.env.${key}`] = JSON.stringify(value);
+ updatedRaw[key] = value;
+ }
+ return acc;
+ },
+ {
+ // Default fallback
+ 'process.env.XSTORYBOOK_EXAMPLE_APP': '""',
+ }
+ );
+ // support destructuring like
+ // const { foo } = import.meta.env;
+ envs['import.meta.env'] = JSON.stringify(stringifyEnvs(updatedRaw));
+
+ return envs;
+}
diff --git a/code/lib/builder-vite/src/index.ts b/code/lib/builder-vite/src/index.ts
new file mode 100644
index 000000000000..a557bc7b878b
--- /dev/null
+++ b/code/lib/builder-vite/src/index.ts
@@ -0,0 +1,94 @@
+// noinspection JSUnusedGlobalSymbols
+
+import * as fs from 'fs';
+import * as path from 'path';
+import type { Builder, StorybookConfig, Options } from '@storybook/core-common';
+import type { RequestHandler, Request, Response } from 'express';
+import type { InlineConfig, UserConfig, ViteDevServer } from 'vite';
+import { transformIframeHtml } from './transform-iframe-html';
+import { createViteServer } from './vite-server';
+import { build as viteBuild } from './build';
+import type { ExtendedOptions } from './types';
+
+// Storybook's Stats are optional Webpack related property
+export type ViteStats = {
+ toJson: () => any;
+};
+
+export type ViteBuilder = Builder;
+
+export type ViteFinal = (
+ config: InlineConfig,
+ options: Options
+) => InlineConfig | Promise;
+
+export type StorybookViteConfig = StorybookConfig & {
+ viteFinal: ViteFinal;
+};
+
+function iframeMiddleware(options: ExtendedOptions, server: ViteDevServer): RequestHandler {
+ return async (req, res, next) => {
+ if (!req.url.match(/^\/iframe\.html($|\?)/)) {
+ next();
+ return;
+ }
+
+ // We need to handle `html-proxy` params for style tag HMR https://github.com/storybookjs/builder-vite/issues/266#issuecomment-1055677865
+ // e.g. /iframe.html?html-proxy&index=0.css
+ if (req.query['html-proxy'] !== undefined) {
+ next();
+ return;
+ }
+
+ const indexHtml = fs.readFileSync(
+ path.resolve(__dirname, '../..', 'input', 'iframe.html'),
+ 'utf-8'
+ );
+ const generated = await transformIframeHtml(indexHtml, options);
+ const transformed = await server.transformIndexHtml('/iframe.html', generated);
+ res.setHeader('Content-Type', 'text/html');
+ res.status(200).send(transformed);
+ };
+}
+
+export const start: ViteBuilder['start'] = async ({
+ startTime,
+ options,
+ router,
+ server: devServer,
+}) => {
+ const server = await createViteServer(options as ExtendedOptions, devServer);
+
+ // Just mock this endpoint (which is really Webpack-specific) so we don't get spammed with 404 in browser devtools
+ // TODO: we should either show some sort of progress from Vite, or just try to disable the whole Loader in the Manager UI.
+ router.get('/progress', (req: Request, res: Response) => {
+ res.header('Cache-Control', 'no-cache');
+ res.header('Content-Type', 'text/event-stream');
+ });
+
+ router.use(await iframeMiddleware(options as ExtendedOptions, server));
+ router.use(server.middlewares);
+
+ async function bail(e?: Error): Promise {
+ try {
+ return await server.close();
+ } catch (err) {
+ console.warn('unable to close vite server');
+ }
+
+ throw e;
+ }
+
+ return {
+ bail,
+ stats: { toJson: () => null },
+ totalTime: process.hrtime(startTime),
+ };
+};
+
+export const build: ViteBuilder['build'] = async ({ options }) => {
+ return viteBuild(options as ExtendedOptions);
+};
+
+export const corePresets = [];
+export const previewPresets = [];
diff --git a/code/lib/builder-vite/src/inject-export-order-plugin.ts b/code/lib/builder-vite/src/inject-export-order-plugin.ts
new file mode 100644
index 000000000000..4afd67cd5111
--- /dev/null
+++ b/code/lib/builder-vite/src/inject-export-order-plugin.ts
@@ -0,0 +1,32 @@
+import { parse } from 'es-module-lexer';
+import MagicString from 'magic-string';
+import { createFilter } from 'vite';
+
+const include = [/\.stories\.([tj])sx?$/, /(stories|story).mdx$/];
+const filter = createFilter(include);
+
+export const injectExportOrderPlugin = {
+ name: 'storybook-vite-inject-export-order-plugin',
+ // This should only run after the typescript has been transpiled
+ enforce: 'post',
+ async transform(code: string, id: string) {
+ if (!filter(id)) return undefined;
+
+ // TODO: Maybe convert `injectExportOrderPlugin` to function that returns object,
+ // and run `await init;` once and then call `parse()` without `await`,
+ // instead of calling `await parse()` every time.
+ const [, exports] = await parse(code);
+
+ if (exports.includes('__namedExportsOrder')) {
+ // user has defined named exports already
+ return undefined;
+ }
+ const s = new MagicString(code);
+ const orderedExports = exports.filter((e) => e !== 'default');
+ s.append(`;export const __namedExportsOrder = ${JSON.stringify(orderedExports)};`);
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ },
+};
diff --git a/code/lib/builder-vite/src/list-stories.ts b/code/lib/builder-vite/src/list-stories.ts
new file mode 100644
index 000000000000..08be02004df1
--- /dev/null
+++ b/code/lib/builder-vite/src/list-stories.ts
@@ -0,0 +1,20 @@
+import * as path from 'path';
+import { promise as glob } from 'glob-promise';
+import { normalizeStories } from '@storybook/core-common';
+
+import type { Options } from '@storybook/core-common';
+
+export async function listStories(options: Options) {
+ return (
+ await Promise.all(
+ normalizeStories(await options.presets.apply('stories', [], options), {
+ configDir: options.configDir,
+ workingDir: options.configDir,
+ }).map(({ directory, files }) => {
+ const pattern = path.join(directory, files);
+
+ return glob(path.isAbsolute(pattern) ? pattern : path.join(options.configDir, pattern));
+ })
+ )
+ ).reduce((carry, stories) => carry.concat(stories), []);
+}
diff --git a/code/lib/builder-vite/src/optimizeDeps.ts b/code/lib/builder-vite/src/optimizeDeps.ts
new file mode 100644
index 000000000000..fbe560720ed4
--- /dev/null
+++ b/code/lib/builder-vite/src/optimizeDeps.ts
@@ -0,0 +1,125 @@
+import * as path from 'path';
+import { normalizePath, resolveConfig, UserConfig } from 'vite';
+import { listStories } from './list-stories';
+
+import type { ExtendedOptions } from './types';
+
+const INCLUDE_CANDIDATES = [
+ '@base2/pretty-print-object',
+ '@emotion/core',
+ '@emotion/is-prop-valid',
+ '@emotion/styled',
+ '@mdx-js/react',
+ '@storybook/addon-docs > acorn-jsx',
+ '@storybook/addon-docs',
+ '@storybook/addons',
+ '@storybook/channel-postmessage',
+ '@storybook/channel-websocket',
+ '@storybook/client-api',
+ '@storybook/client-logger',
+ '@storybook/core/client',
+ '@storybook/csf',
+ '@storybook/preview-web',
+ '@storybook/react > acorn-jsx',
+ '@storybook/react',
+ '@storybook/svelte',
+ '@storybook/vue3',
+ 'acorn-jsx',
+ 'acorn-walk',
+ 'acorn',
+ 'airbnb-js-shims',
+ 'ansi-to-html',
+ 'axe-core',
+ 'color-convert',
+ 'deep-object-diff',
+ 'doctrine',
+ 'emotion-theming',
+ 'escodegen',
+ 'estraverse',
+ 'fast-deep-equal',
+ 'global',
+ 'html-tags',
+ 'isobject',
+ 'jest-mock',
+ 'loader-utils',
+ 'lodash/cloneDeep',
+ 'lodash/isFunction',
+ 'lodash/isPlainObject',
+ 'lodash/isString',
+ 'lodash/mapKeys',
+ 'lodash/mapValues',
+ 'lodash/pick',
+ 'lodash/pickBy',
+ 'lodash/startCase',
+ 'lodash/throttle',
+ 'lodash/uniq',
+ 'markdown-to-jsx',
+ 'memoizerific',
+ 'overlayscrollbars',
+ 'polished',
+ 'prettier/parser-babel',
+ 'prettier/parser-flow',
+ 'prettier/parser-typescript',
+ 'prop-types',
+ 'qs',
+ 'react-dom',
+ 'react-dom/client',
+ 'react-fast-compare',
+ 'react-is',
+ 'react-textarea-autosize',
+ 'react',
+ 'react/jsx-runtime',
+ 'refractor/core',
+ 'refractor/lang/bash.js',
+ 'refractor/lang/css.js',
+ 'refractor/lang/graphql.js',
+ 'refractor/lang/js-extras.js',
+ 'refractor/lang/json.js',
+ 'refractor/lang/jsx.js',
+ 'refractor/lang/markdown.js',
+ 'refractor/lang/markup.js',
+ 'refractor/lang/tsx.js',
+ 'refractor/lang/typescript.js',
+ 'refractor/lang/yaml.js',
+ 'regenerator-runtime/runtime.js',
+ 'slash',
+ 'stable',
+ 'store2',
+ 'synchronous-promise',
+ 'telejson',
+ 'ts-dedent',
+ 'unfetch',
+ 'util-deprecate',
+ 'uuid-browser/v4',
+ 'vue',
+ 'warning',
+];
+
+/**
+ * Helper function which allows us to `filter` with an async predicate. Uses Promise.all for performance.
+ */
+const asyncFilter = async (arr: string[], predicate: (val: string) => Promise) =>
+ Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index]));
+
+export async function getOptimizeDeps(
+ config: UserConfig & { configFile: false; root: string },
+ options: ExtendedOptions
+) {
+ const { root } = config;
+ const absoluteStories = await listStories(options);
+ const stories = absoluteStories.map((storyPath) => normalizePath(path.relative(root, storyPath)));
+ const resolvedConfig = await resolveConfig(config, 'serve', 'development');
+
+ // This function converts ids which might include ` > ` to a real path, if it exists on disk.
+ // See https://github.com/vitejs/vite/blob/67d164392e8e9081dc3f0338c4b4b8eea6c5f7da/packages/vite/src/node/optimizer/index.ts#L182-L199
+ const resolve = resolvedConfig.createResolver({ asSrc: false });
+ const include = await asyncFilter(INCLUDE_CANDIDATES, async (id) => Boolean(await resolve(id)));
+
+ return {
+ // We don't need to resolve the glob since vite supports globs for entries.
+ entries: stories,
+ // We need Vite to precompile these dependencies, because they contain non-ESM code that would break
+ // if we served it directly to the browser.
+ include,
+ };
+}
diff --git a/code/lib/builder-vite/src/plugins/docgen-handlers/actualNameHandler.ts b/code/lib/builder-vite/src/plugins/docgen-handlers/actualNameHandler.ts
new file mode 100644
index 000000000000..ddf860b37ee8
--- /dev/null
+++ b/code/lib/builder-vite/src/plugins/docgen-handlers/actualNameHandler.ts
@@ -0,0 +1,51 @@
+/**
+ * This is heavily based on the react-docgen `displayNameHandler`
+ * (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
+ * but instead defines an `actualName` property on the generated docs that is taken first from the component's actual name.
+ * This addresses an issue where the name that the generated docs are stored under is incorrectly named with the `displayName`
+ * and not the component's actual name.
+ *
+ * This is inspired by `actualNameHandler` from https://github.com/storybookjs/babel-plugin-react-docgen, but is modified
+ * directly from displayNameHandler, using the same approach as babel-plugin-react-docgen.
+ */
+
+import { namedTypes as t } from 'ast-types';
+import type { NodePath } from 'ast-types/lib/node-path';
+import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/dist/utils';
+// import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/lib/utils';
+import type { Importer } from 'react-docgen/dist/parse';
+// import type { Importer } from 'react-docgen/lib/parse';
+import type Documentation from 'react-docgen/lib/Documentation';
+
+export default function actualNameHandler(
+ documentation: Documentation,
+ path: NodePath,
+ importer: Importer
+): void {
+ if (t.ClassDeclaration.check(path.node) || t.FunctionDeclaration.check(path.node)) {
+ documentation.set('actualName', getNameOrValue(path.get('id')));
+ } else if (
+ t.ArrowFunctionExpression.check(path.node) ||
+ t.FunctionExpression.check(path.node) ||
+ isReactForwardRefCall(path, importer)
+ ) {
+ let currentPath = path;
+ while (currentPath.parent) {
+ if (t.VariableDeclarator.check(currentPath.parent.node)) {
+ documentation.set('actualName', getNameOrValue(currentPath.parent.get('id')));
+ return;
+ }
+
+ if (t.AssignmentExpression.check(currentPath.parent.node)) {
+ const leftPath = currentPath.parent.get('left');
+ if (t.Identifier.check(leftPath.node) || t.Literal.check(leftPath.node)) {
+ documentation.set('actualName', getNameOrValue(leftPath));
+ return;
+ }
+ }
+ currentPath = currentPath.parent;
+ }
+ // Could not find an actual name
+ documentation.set('actualName', '');
+ }
+}
diff --git a/code/lib/builder-vite/src/plugins/mdx-plugin.ts b/code/lib/builder-vite/src/plugins/mdx-plugin.ts
new file mode 100644
index 000000000000..eee7043481ab
--- /dev/null
+++ b/code/lib/builder-vite/src/plugins/mdx-plugin.ts
@@ -0,0 +1,82 @@
+import type { Options } from '@storybook/core-common';
+import type { Plugin } from 'vite';
+import { createFilter } from 'vite';
+
+const isStorybookMdx = (id: string) => id.endsWith('stories.mdx') || id.endsWith('story.mdx');
+
+function injectRenderer(code: string, mdx2: boolean) {
+ if (mdx2) {
+ return `
+ import React from 'react';
+ ${code}
+ `;
+ }
+
+ return `
+ /* @jsx mdx */
+ import React from 'react';
+ import { mdx } from '@mdx-js/react';
+ ${code}
+ `;
+}
+
+/**
+ * Storybook uses two different loaders when dealing with MDX:
+ *
+ * - *stories.mdx and *story.mdx are compiled with the CSF compiler
+ * - *.mdx are compiled with the MDX compiler directly
+ *
+ * @see https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/recipes.md#csf-stories-with-arbitrary-mdx
+ */
+export function mdxPlugin(options: Options): Plugin {
+ const { features } = options;
+
+ let reactRefresh: Plugin | undefined;
+ const include = /\.mdx?$/;
+ const filter = createFilter(include);
+
+ return {
+ name: 'storybook-vite-mdx-plugin',
+ enforce: 'pre',
+ configResolved({ plugins }) {
+ // @vitejs/plugin-react-refresh has been upgraded to @vitejs/plugin-react,
+ // and the name of the plugin performing `transform` has been changed from 'react-refresh' to 'vite:react-babel',
+ // to be compatible, we need to look for both plugin name.
+ // We should also look for the other plugins names exported from @vitejs/plugin-react in case there are some internal refactors.
+ const reactRefreshPlugins = plugins.filter(
+ (p) =>
+ p.name === 'react-refresh' ||
+ p.name === 'vite:react-babel' ||
+ p.name === 'vite:react-refresh' ||
+ p.name === 'vite:react-jsx'
+ );
+ reactRefresh = reactRefreshPlugins.find((p) => p.transform);
+ },
+ async transform(src, id, options) {
+ if (!filter(id)) return undefined;
+
+ // @ts-expect-error typescript doesn't think compile exists, but it does.
+ const { compile } = features?.previewMdx2
+ ? await import('@storybook/mdx2-csf')
+ : await import('@storybook/mdx1-csf');
+
+ const mdxCode = String(await compile(src, { skipCsf: !isStorybookMdx(id) }));
+
+ const modifiedCode = injectRenderer(mdxCode, Boolean(features?.previewMdx2));
+
+ const result = await reactRefresh?.transform!.call(this, modifiedCode, `${id}.jsx`, options);
+
+ if (!result) return modifiedCode;
+
+ if (typeof result === 'string') return result;
+
+ const { code, map: resultMap } = result;
+
+ return {
+ code,
+ map:
+ !resultMap || typeof resultMap === 'string' ? resultMap : { ...resultMap, sources: [id] },
+ };
+ },
+ };
+}
diff --git a/code/lib/builder-vite/src/plugins/no-fouc.ts b/code/lib/builder-vite/src/plugins/no-fouc.ts
new file mode 100644
index 000000000000..8e741db2464f
--- /dev/null
+++ b/code/lib/builder-vite/src/plugins/no-fouc.ts
@@ -0,0 +1,52 @@
+import type { Plugin } from 'vite';
+
+/**
+ * This plugin is a workaround to inject some styles into the `` of the iframe to
+ * prevent the "no story" text from appearing breifly while the page loads in.
+ *
+ * It can be removed, and these styles placed back into the head,
+ * when https://github.com/vitejs/vite/issues/6737 is closed.
+ */
+export function noFouc(): Plugin {
+ return {
+ name: 'no-fouc',
+ enforce: 'post',
+ async transformIndexHtml(html, ctx) {
+ if (ctx.path !== '/iframe.html') return undefined;
+
+ return insertHeadStyles(html);
+ },
+ };
+}
+
+/**
+ * Insert default styles to hide storybook elements as the page loads until JS can
+ * add the official storybook default head styles and scripts. These lines are mostly
+ * taken from https://github.com/storybookjs/storybook/blob/next/lib/core-common/templates/base-preview-head.html#L6-L20
+ */
+function insertHeadStyles(html: string) {
+ return html.replace(
+ '',
+ `
+
+
+ `.trim()
+ );
+}
diff --git a/code/lib/builder-vite/src/plugins/react-docgen.ts b/code/lib/builder-vite/src/plugins/react-docgen.ts
new file mode 100644
index 000000000000..d3e0ae64766c
--- /dev/null
+++ b/code/lib/builder-vite/src/plugins/react-docgen.ts
@@ -0,0 +1,69 @@
+import path from 'path';
+import {
+ parse,
+ handlers as docgenHandlers,
+ resolver as docgenResolver,
+ importers as docgenImporters,
+} from 'react-docgen';
+import type { DocumentationObject } from 'react-docgen/lib/Documentation';
+import MagicString from 'magic-string';
+import type { Plugin } from 'vite';
+import { createFilter } from 'vite';
+import actualNameHandler from './docgen-handlers/actualNameHandler';
+
+type DocObj = DocumentationObject & { actualName: string };
+
+// TODO: None of these are able to be overridden, so `default` is aspirational here.
+const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler);
+const defaultResolver = docgenResolver.findAllExportedComponentDefinitions;
+const defaultImporter = docgenImporters.makeFsImporter();
+const handlers = [...defaultHandlers, actualNameHandler];
+
+type Options = {
+ include?: string | RegExp | (string | RegExp)[];
+ exclude?: string | RegExp | (string | RegExp)[];
+};
+
+export function reactDocgen({
+ include = /\.(mjs|tsx?|jsx?)$/,
+ exclude = [/node_modules\/.*/, '**/**.stories.tsx'],
+}: Options = {}): Plugin {
+ const cwd = process.cwd();
+ const filter = createFilter(include, exclude);
+
+ return {
+ name: 'react-docgen',
+ enforce: 'pre',
+ async transform(src: string, id: string) {
+ const relPath = path.relative(cwd, id);
+ if (!filter(relPath)) return undefined;
+
+ try {
+ // Since we're using `findAllExportedComponentDefinitions`, this will always be an array.
+ const docgenResults = parse(src, defaultResolver, handlers, {
+ importer: defaultImporter,
+ filename: id,
+ }) as DocObj[];
+ const s = new MagicString(src);
+
+ docgenResults.forEach((info) => {
+ const { actualName, ...docgenInfo } = info;
+ if (actualName) {
+ const docNode = JSON.stringify(docgenInfo);
+ s.append(`;${actualName}.__docgenInfo=${docNode}`);
+ }
+ });
+
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ } catch (e) {
+ // Usually this is just an error from react-docgen that it couldn't find a component
+ // Only uncomment for troubleshooting
+ // console.error(e);
+ return undefined;
+ }
+ },
+ };
+}
diff --git a/code/lib/builder-vite/src/plugins/svelte-docgen.ts b/code/lib/builder-vite/src/plugins/svelte-docgen.ts
new file mode 100644
index 000000000000..22378e1a6658
--- /dev/null
+++ b/code/lib/builder-vite/src/plugins/svelte-docgen.ts
@@ -0,0 +1,102 @@
+import type { Plugin } from 'vite';
+import MagicString from 'magic-string';
+import path from 'path';
+import fs from 'fs';
+import svelteDoc from 'sveltedoc-parser';
+import type { SvelteParserOptions } from 'sveltedoc-parser';
+import { logger } from '@storybook/node-logger';
+import { preprocess } from 'svelte/compiler';
+import { createFilter } from 'vite';
+
+// Most of the code here should probably be exported by @storybook/svelte and reused here.
+// See: https://github.com/storybookjs/storybook/blob/next/app/svelte/src/server/svelte-docgen-loader.ts
+
+// From https://github.com/sveltejs/svelte/blob/8db3e8d0297e052556f0b6dde310ef6e197b8d18/src/compiler/compile/utils/get_name_from_filename.ts
+// Copied because it is not exported from the compiler
+function getNameFromFilename(filename: string) {
+ if (!filename) return null;
+
+ const parts = filename.split(/[/\\]/).map(encodeURI);
+
+ if (parts.length > 1) {
+ const indexMatch = parts[parts.length - 1].match(/^index(\.\w+)/);
+ if (indexMatch) {
+ parts.pop();
+ parts[parts.length - 1] += indexMatch[1];
+ }
+ }
+
+ const base = parts
+ .pop()
+ ?.replace(/%/g, 'u')
+ .replace(/\.[^.]+$/, '')
+ .replace(/[^a-zA-Z_$0-9]+/g, '_')
+ .replace(/^_/, '')
+ .replace(/_$/, '')
+ .replace(/^(\d)/, '_$1');
+
+ if (!base) {
+ throw new Error(`Could not derive component name from file ${filename}`);
+ }
+
+ return base[0].toUpperCase() + base.slice(1);
+}
+
+export function svelteDocgen(svelteOptions: Record): Plugin {
+ const cwd = process.cwd();
+ const { preprocess: preprocessOptions, logDocgen = false } = svelteOptions;
+ const include = /\.(svelte)$/;
+ const filter = createFilter(include);
+
+ return {
+ name: 'svelte-docgen',
+ async transform(src: string, id: string) {
+ if (!filter(id)) return undefined;
+
+ const resource = path.relative(cwd, id);
+
+ let docOptions;
+ if (preprocessOptions) {
+ const src = fs.readFileSync(resource).toString();
+
+ const { code: fileContent } = await preprocess(src, preprocessOptions, {
+ filename: resource,
+ });
+
+ docOptions = {
+ fileContent,
+ };
+ } else {
+ docOptions = { filename: resource };
+ }
+
+ // set SvelteDoc options
+ const options: SvelteParserOptions = {
+ ...docOptions,
+ version: 3,
+ };
+
+ const s = new MagicString(src);
+
+ try {
+ const componentDoc = await svelteDoc.parse(options);
+ // get filename for source content
+ const file = path.basename(resource);
+
+ componentDoc.name = path.basename(file);
+
+ const componentName = getNameFromFilename(resource);
+ s.append(`;${componentName}.__docgen = ${JSON.stringify(componentDoc)}`);
+ } catch (error: any) {
+ if (logDocgen) {
+ logger.error(error);
+ }
+ }
+
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ },
+ };
+}
diff --git a/code/lib/builder-vite/src/source-loader-plugin.ts b/code/lib/builder-vite/src/source-loader-plugin.ts
new file mode 100644
index 000000000000..2192653c9097
--- /dev/null
+++ b/code/lib/builder-vite/src/source-loader-plugin.ts
@@ -0,0 +1,103 @@
+import type { Plugin } from 'vite';
+import sourceLoaderTransform from '@storybook/source-loader';
+import MagicString from 'magic-string';
+import type { ExtendedOptions } from './types';
+
+const storyPattern = /\.stories\.[jt]sx?$/;
+const storySourcePattern = /var __STORY__ = "(.*)"/;
+const storySourceReplacement = '--STORY_SOURCE_REPLACEMENT--';
+
+const mockClassLoader = (id: string) => ({
+ // eslint-disable-next-line no-console
+ emitWarning: (message: string) => console.warn(message),
+ resourcePath: id,
+});
+
+// HACK: Until we can support only node 15+ and use string.prototype.replaceAll
+const replaceAll = (str: string, search: string, replacement: string) => {
+ return str.split(search).join(replacement);
+};
+
+export function sourceLoaderPlugin(config: ExtendedOptions): Plugin | Plugin[] {
+ if (config.configType === 'DEVELOPMENT') {
+ return {
+ name: 'storybook-vite-source-loader-plugin',
+ enforce: 'pre',
+ async transform(src: string, id: string) {
+ if (id.match(storyPattern)) {
+ const code: string = await sourceLoaderTransform.call(mockClassLoader(id), src);
+ const s = new MagicString(src);
+ // Entirely replace with new code
+ s.overwrite(0, src.length, code);
+
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ }
+ return undefined;
+ },
+ };
+ }
+
+ // In production, we need to be fancier, to avoid vite:define plugin from replacing values inside the `__STORY__` string
+ const storySources = new WeakMap>();
+
+ return [
+ {
+ name: 'storybook-vite-source-loader-plugin',
+ enforce: 'pre',
+ buildStart() {
+ storySources.set(config, new Map());
+ },
+ async transform(src: string, id: string) {
+ if (id.match(storyPattern)) {
+ let code: string = await sourceLoaderTransform.call(mockClassLoader(id), src);
+ const [_, sourceString] = code.match(storySourcePattern) ?? [null, null];
+ if (sourceString) {
+ const map = storySources.get(config);
+ map?.set(id, sourceString);
+
+ // Remove story source so that it is not processed by vite:define plugin
+ code = replaceAll(code, sourceString, storySourceReplacement);
+ }
+
+ const s = new MagicString(src);
+ // Entirely replace with new code
+ s.overwrite(0, src.length, code);
+
+ return {
+ code: s.toString(),
+ map: s.generateMap(),
+ };
+ }
+ return undefined;
+ },
+ },
+ {
+ name: 'storybook-vite-source-loader-plugin-post',
+ enforce: 'post',
+ buildStart() {
+ storySources.set(config, new Map());
+ },
+ async transform(src: string, id: string) {
+ if (id.match(storyPattern)) {
+ const s = new MagicString(src);
+ const map = storySources.get(config);
+ const storySourceStatement = map?.get(id);
+ // Put the previously-extracted source back in
+ if (storySourceStatement) {
+ const newCode = replaceAll(src, storySourceReplacement, storySourceStatement);
+ s.overwrite(0, src.length, newCode);
+ }
+
+ return {
+ code: s.toString(),
+ map: s.generateMap(),
+ };
+ }
+ return undefined;
+ },
+ },
+ ];
+}
diff --git a/code/lib/builder-vite/src/svelte/csf-plugin.ts b/code/lib/builder-vite/src/svelte/csf-plugin.ts
new file mode 100644
index 000000000000..ce6cfe9c1561
--- /dev/null
+++ b/code/lib/builder-vite/src/svelte/csf-plugin.ts
@@ -0,0 +1,59 @@
+import { getNameFromFilename } from '@storybook/addon-svelte-csf/dist/cjs/parser/svelte-stories-loader';
+import { readFileSync } from 'fs';
+import { extractStories } from '@storybook/addon-svelte-csf/dist/cjs/parser/extract-stories';
+import type { Options } from '@sveltejs/vite-plugin-svelte';
+import * as svelte from 'svelte/compiler';
+import MagicString from 'magic-string';
+import { createFilter } from 'vite';
+
+const parser = require
+ .resolve('@storybook/addon-svelte-csf/dist/esm/parser/collect-stories')
+ .replace(/[/\\]/g, '/');
+
+export default function csfPlugin(svelteOptions?: Options) {
+ const include = /\.stories\.svelte$/;
+ const filter = createFilter(include);
+
+ return {
+ name: 'storybook-addon-svelte-csf',
+ enforce: 'post',
+ async transform(code: string, id: string) {
+ if (!filter(id)) return undefined;
+
+ const s = new MagicString(code);
+ const component = getNameFromFilename(id);
+ let source = readFileSync(id).toString();
+ if (svelteOptions && svelteOptions.preprocess) {
+ source = (await svelte.preprocess(source, svelteOptions.preprocess, { filename: id })).code;
+ }
+ const all = extractStories(source);
+ const { stories } = all;
+ const storyDef = Object.entries(stories)
+ .filter(([, def]) => !def.template)
+ .map(([id]) => `export const ${id} = __storiesMetaData.stories[${JSON.stringify(id)}];`)
+ .join('\n');
+
+ s.replace('export default', '// export default');
+
+ const namedExportsOrder = Object.entries(stories)
+ .filter(([, def]) => !def.template)
+ .map(([id]) => id);
+
+ const output = [
+ '',
+ `import parser from '${parser}';`,
+ `const __storiesMetaData = parser(${component}, ${JSON.stringify(all)});`,
+ 'export default __storiesMetaData.meta;',
+ `export const __namedExportsOrder = ${JSON.stringify(namedExportsOrder)};`,
+ storyDef,
+ ].join('\n');
+
+ s.append(output);
+
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true, source: id }),
+ };
+ },
+ };
+}
diff --git a/code/lib/builder-vite/src/transform-iframe-html.ts b/code/lib/builder-vite/src/transform-iframe-html.ts
new file mode 100644
index 000000000000..4af39393f77b
--- /dev/null
+++ b/code/lib/builder-vite/src/transform-iframe-html.ts
@@ -0,0 +1,36 @@
+import { normalizeStories } from '@storybook/core-common';
+import type { CoreConfig } from '@storybook/core-common';
+import type { ExtendedOptions } from './types';
+
+export type PreviewHtml = string | undefined;
+
+export async function transformIframeHtml(html: string, options: ExtendedOptions) {
+ const { configType, features, framework, presets, serverChannelUrl, title } = options;
+ const headHtmlSnippet = await presets.apply('previewHead');
+ const bodyHtmlSnippet = await presets.apply('previewBody');
+ const logLevel = await presets.apply('logLevel', undefined);
+ const frameworkOptions = await presets.apply(`${framework}Options`, {});
+ const coreOptions = await presets.apply('core');
+ const stories = normalizeStories(await options.presets.apply('stories', [], options), {
+ configDir: options.configDir,
+ workingDir: process.cwd(),
+ }).map((specifier) => ({
+ ...specifier,
+ importPathMatcher: specifier.importPathMatcher.source,
+ }));
+
+ return html
+ .replace('', title || 'Storybook')
+ .replace('[CONFIG_TYPE HERE]', configType || '')
+ .replace('[LOGLEVEL HERE]', logLevel || '')
+ .replace(`'[FRAMEWORK_OPTIONS HERE]'`, JSON.stringify(frameworkOptions || {}))
+ .replace(
+ `'[CHANNEL_OPTIONS HERE]'`,
+ JSON.stringify(coreOptions && coreOptions.channelOptions ? coreOptions.channelOptions : {})
+ )
+ .replace(`'[FEATURES HERE]'`, JSON.stringify(features || {}))
+ .replace(`'[STORIES HERE]'`, JSON.stringify(stories || {}))
+ .replace(`'[SERVER_CHANNEL_URL HERE]'`, JSON.stringify(serverChannelUrl))
+ .replace('', headHtmlSnippet || '')
+ .replace('', bodyHtmlSnippet || '');
+}
diff --git a/code/lib/builder-vite/src/types/envs-raw.type.ts b/code/lib/builder-vite/src/types/envs-raw.type.ts
new file mode 100644
index 000000000000..cb62e513aacd
--- /dev/null
+++ b/code/lib/builder-vite/src/types/envs-raw.type.ts
@@ -0,0 +1 @@
+export type EnvsRaw = Record;
diff --git a/code/lib/builder-vite/src/types/extended-options.type.ts b/code/lib/builder-vite/src/types/extended-options.type.ts
new file mode 100644
index 000000000000..7b582f115c20
--- /dev/null
+++ b/code/lib/builder-vite/src/types/extended-options.type.ts
@@ -0,0 +1,11 @@
+import type { Options } from '@storybook/core-common';
+
+// Using instead of `Record` to provide better aware of used options
+type IframeOptions = {
+ frameworkPath: string;
+ title: string;
+ // FIXME: Use @ndelangen's improved types
+ framework: string;
+};
+
+export type ExtendedOptions = Options & IframeOptions;
diff --git a/code/lib/builder-vite/src/types/index.ts b/code/lib/builder-vite/src/types/index.ts
new file mode 100644
index 000000000000..e50c278bbd1e
--- /dev/null
+++ b/code/lib/builder-vite/src/types/index.ts
@@ -0,0 +1,2 @@
+export * from './envs-raw.type';
+export * from './extended-options.type';
diff --git a/code/lib/builder-vite/src/types/react-docgen.d.ts b/code/lib/builder-vite/src/types/react-docgen.d.ts
new file mode 100644
index 000000000000..cc2a8d6322fb
--- /dev/null
+++ b/code/lib/builder-vite/src/types/react-docgen.d.ts
@@ -0,0 +1,22 @@
+// TODO: delete this stub file once a new alpha of react-docgen is released (will include ts types).
+
+declare module 'react-docgen' {
+ declare const parse;
+ declare const handlers;
+ declare const resolver;
+ declare const importers;
+}
+
+declare module 'react-docgen/lib/Documentation' {
+ export type DocumentationObject = Record;
+ export default Documentation;
+}
+
+declare module 'react-docgen/dist/utils' {
+ declare const getNameOrValue;
+ declare const isReactForwardRefCall;
+}
+
+declare module 'react-docgen/dist/parse' {
+ declare type Importer = any;
+}
diff --git a/code/lib/builder-vite/src/utils/transform-abs-path.ts b/code/lib/builder-vite/src/utils/transform-abs-path.ts
new file mode 100644
index 000000000000..bd8c89f8fbc3
--- /dev/null
+++ b/code/lib/builder-vite/src/utils/transform-abs-path.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+import { normalizePath } from 'vite';
+
+// We need to convert from an absolute path, to a traditional node module import path,
+// so that vite can correctly pre-bundle/optimize
+export function transformAbsPath(absPath: string) {
+ const splits = absPath.split(`node_modules${path.sep}`);
+ // Return everything after the final "node_modules/"
+ const module = normalizePath(splits[splits.length - 1]);
+ return module;
+}
diff --git a/code/lib/builder-vite/src/virtual-file-names.ts b/code/lib/builder-vite/src/virtual-file-names.ts
new file mode 100644
index 000000000000..0da0c5517dec
--- /dev/null
+++ b/code/lib/builder-vite/src/virtual-file-names.ts
@@ -0,0 +1,4 @@
+export const virtualFileId = '/virtual:/@storybook/builder-vite/vite-app.js';
+export const virtualStoriesFile = '/virtual:/@storybook/builder-vite/storybook-stories.js';
+export const virtualPreviewFile = '/virtual:/@storybook/builder-vite/preview-entry.js';
+export const virtualAddonSetupFile = '/virtual:/@storybook/builder-vite/setup-addons.js';
diff --git a/code/lib/builder-vite/src/vite-config.ts b/code/lib/builder-vite/src/vite-config.ts
new file mode 100644
index 000000000000..79abbb9b6bfb
--- /dev/null
+++ b/code/lib/builder-vite/src/vite-config.ts
@@ -0,0 +1,154 @@
+import * as path from 'path';
+import fs from 'fs';
+import { Plugin } from 'vite';
+import viteReact from '@vitejs/plugin-react';
+import type { UserConfig } from 'vite';
+import type { TypescriptOptions } from '@storybook/core-vite';
+import { allowedEnvPrefix as envPrefix } from './envs';
+import { codeGeneratorPlugin } from './code-generator-plugin';
+import { injectExportOrderPlugin } from './inject-export-order-plugin';
+import { mdxPlugin } from './plugins/mdx-plugin';
+import { noFouc } from './plugins/no-fouc';
+import { sourceLoaderPlugin } from './source-loader-plugin';
+import type { ExtendedOptions } from './types';
+
+export type PluginConfigType = 'build' | 'development';
+
+export function readPackageJson(): Record | false {
+ const packageJsonPath = path.resolve('package.json');
+ if (!fs.existsSync(packageJsonPath)) {
+ return false;
+ }
+
+ const jsonContent = fs.readFileSync(packageJsonPath, 'utf8');
+ return JSON.parse(jsonContent);
+}
+
+// Vite config that is common to development and production mode
+export async function commonConfig(
+ options: ExtendedOptions,
+ _type: PluginConfigType
+): Promise {
+ const { framework } = options;
+
+ return {
+ configFile: false,
+ root: path.resolve(options.configDir, '..'),
+ cacheDir: 'node_modules/.vite-storybook',
+ envPrefix,
+ define: {},
+ plugins: await pluginConfig(options, _type),
+ };
+}
+
+export async function pluginConfig(options: ExtendedOptions, _type: PluginConfigType) {
+ const { framework, presets } = options;
+ const svelteOptions: Record = await presets.apply('svelteOptions', {}, options);
+
+ const plugins = [
+ codeGeneratorPlugin(options),
+ // sourceLoaderPlugin(options),
+ mdxPlugin(options),
+ noFouc(),
+ injectExportOrderPlugin,
+ // We need the react plugin here to support MDX.
+ viteReact({
+ // Do not treat story files as HMR boundaries, storybook itself needs to handle them.
+ exclude: [/\.stories\.([tj])sx?$/, /node_modules/].concat(
+ framework === 'react' ? [] : [/\.([tj])sx?$/]
+ ),
+ }),
+ {
+ name: 'vite-plugin-storybook-allow',
+ enforce: 'post',
+ config(config) {
+ // if there is no allow list then Vite allows anything in the root directory
+ // if there is an allow list then Vite allows anything in the listed directories
+ // add the .storybook directory only if there's an allow list so that we don't end up
+ // disallowing the root directory unless it's already disallowed
+ if (config?.server?.fs?.allow) {
+ config.server.fs.allow.push('.storybook');
+ }
+ },
+ },
+ ] as Plugin[];
+ if (framework === 'svelte') {
+ try {
+ // eslint-disable-next-line import/no-extraneous-dependencies, global-require
+ const sveltePlugin = require('@sveltejs/vite-plugin-svelte').svelte;
+
+ // We need to create two separate svelte plugins, one for stories, and one for other svelte files
+ // because stories.svelte files cannot be hot-module-reloaded.
+ // Suggested in: https://github.com/sveltejs/vite-plugin-svelte/issues/321#issuecomment-1113205509
+
+ // First, create an array containing user exclude patterns, to combine with ours.
+
+ let userExclude = [];
+ if (Array.isArray(svelteOptions?.exclude)) {
+ userExclude = svelteOptions?.exclude;
+ } else if (svelteOptions?.exclude) {
+ userExclude = [svelteOptions?.exclude];
+ }
+
+ // These are the svelte stories we need to exclude from HMR
+ const storyPatterns = ['**/*.story.svelte', '**/*.stories.svelte'];
+ // Non-story svelte files
+ // Starting in 1.0.0-next.42, svelte.config.js is included by default.
+ // We disable that, but allow it to be overridden in svelteOptions
+ plugins.push(sveltePlugin({ ...svelteOptions, exclude: [...userExclude, ...storyPatterns] }));
+ // Svelte stories without HMR
+ const storySveltePlugin = sveltePlugin({
+ ...svelteOptions,
+ exclude: userExclude,
+ include: storyPatterns,
+ hot: false,
+ });
+ plugins.push({
+ // Starting in 1.0.0-next.43, the plugin function returns an array of plugins. We only want the first one here.
+ ...(Array.isArray(storySveltePlugin) ? storySveltePlugin[0] : storySveltePlugin),
+ name: 'vite-plugin-svelte-stories',
+ });
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') {
+ throw new Error(
+ '@storybook/builder-vite requires @sveltejs/vite-plugin-svelte to be installed' +
+ ' when using @storybook/svelte.' +
+ ' Please install it and start storybook again.'
+ );
+ }
+ throw err;
+ }
+
+ // eslint-disable-next-line import/no-extraneous-dependencies, global-require
+ const { loadSvelteConfig } = require('@sveltejs/vite-plugin-svelte');
+ const config = { ...loadSvelteConfig(), ...svelteOptions };
+
+ try {
+ // eslint-disable-next-line global-require
+ const csfPlugin = require('./svelte/csf-plugin').default;
+ plugins.push(csfPlugin(config));
+ } catch (err) {
+ // Not all projects use `.stories.svelte` for stories, and by default 6.5+ does not auto-install @storybook/addon-svelte-csf.
+ // If it's any other kind of error, re-throw.
+ if ((err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') {
+ throw err;
+ }
+ }
+
+ const { svelteDocgen } = await import('./plugins/svelte-docgen');
+ plugins.push(svelteDocgen(config));
+ }
+
+ if (framework === 'preact') {
+ // eslint-disable-next-line global-require
+ plugins.push(require('@preact/preset-vite').default());
+ }
+
+ if (framework === 'glimmerx') {
+ // eslint-disable-next-line global-require, import/extensions
+ const plugin = require('vite-plugin-glimmerx/index.cjs');
+ plugins.push(plugin.default());
+ }
+
+ return plugins;
+}
diff --git a/code/lib/builder-vite/src/vite-server.ts b/code/lib/builder-vite/src/vite-server.ts
new file mode 100644
index 000000000000..26ecbc4d51ad
--- /dev/null
+++ b/code/lib/builder-vite/src/vite-server.ts
@@ -0,0 +1,40 @@
+import type { Server } from 'http';
+import { createServer } from 'vite';
+import { stringifyProcessEnvs } from './envs';
+import { getOptimizeDeps } from './optimizeDeps';
+import { commonConfig } from './vite-config';
+import type { EnvsRaw, ExtendedOptions } from './types';
+
+export async function createViteServer(options: ExtendedOptions, devServer: Server) {
+ const { port, presets } = options;
+
+ const baseConfig = await commonConfig(options, 'development');
+ const defaultConfig = {
+ ...baseConfig,
+ server: {
+ middlewareMode: true,
+ hmr: {
+ port,
+ server: devServer,
+ },
+ fs: {
+ strict: true,
+ },
+ },
+ appType: 'custom' as const,
+ optimizeDeps: await getOptimizeDeps(baseConfig, options),
+ };
+
+ const finalConfig = await presets.apply('viteFinal', defaultConfig, options);
+
+ const envsRaw = await presets.apply>('env');
+ // Stringify env variables after getting `envPrefix` from the final config
+ const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix);
+ // Update `define`
+ finalConfig.define = {
+ ...finalConfig.define,
+ ...envs,
+ };
+
+ return createServer(finalConfig);
+}
diff --git a/code/lib/builder-vite/tsconfig.json b/code/lib/builder-vite/tsconfig.json
new file mode 100644
index 000000000000..fb09353cd547
--- /dev/null
+++ b/code/lib/builder-vite/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "rootDir": ".",
+ "outDir": "dist",
+ "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"],
+ "module": "CommonJS",
+ "target": "ES2019",
+ "declaration": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "types": ["@types/node"]
+ }
+}
+
+
diff --git a/code/lib/channel-websocket/src/index.ts b/code/lib/channel-websocket/src/index.ts
index 87b5cce0b903..fb5628be5032 100644
--- a/code/lib/channel-websocket/src/index.ts
+++ b/code/lib/channel-websocket/src/index.ts
@@ -85,3 +85,6 @@ export function createChannel({
const transport = new WebsocketTransport({ url, onError });
return new Channel({ transport, async });
}
+
+// backwards compat with builder-vite
+export default createChannel;
diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json
index aa0f7b2d3629..f16aa9b3d1de 100644
--- a/code/lib/cli/package.json
+++ b/code/lib/cli/package.json
@@ -22,13 +22,6 @@
},
"license": "MIT",
"author": "Storybook Team",
- "typesVersions": {
- "<3.8": {
- "*": [
- "ts3.4/*"
- ]
- }
- },
"bin": {
"getstorybook": "./bin/index.js",
"sb": "./bin/index.js"
diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts
index 2227fbb5040c..18b6a7202410 100644
--- a/code/lib/cli/src/generators/baseGenerator.ts
+++ b/code/lib/cli/src/generators/baseGenerator.ts
@@ -212,7 +212,7 @@ export async function baseGenerator(
}
// FIXME: temporary workaround for https://github.com/storybookjs/storybook/issues/17516
- if (frameworkPackages.includes('@storybook/builder-vite')) {
+ if (frameworkPackages.find((pkg) => pkg.match(/^@storybook\/.*-vite$/))) {
const previewHead = dedent`