├── .nvmrc ├── .prettierrc ├── example ├── .gitignore ├── src │ ├── assets │ │ └── favicon.ico │ ├── components │ │ ├── SolidLogo │ │ │ ├── index.tsx │ │ │ ├── index.stories.ts │ │ │ └── solid-logo.svg │ │ ├── Logos │ │ │ ├── plus.svg │ │ │ ├── index.tsx │ │ │ └── index.stories.ts │ │ ├── StorybookLogo │ │ │ ├── index.tsx │ │ │ ├── index.stories.ts │ │ │ └── storybook-logo.svg │ │ └── Link │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── index.stories.ts │ ├── index.tsx │ ├── App.module.css │ ├── index.css │ └── App.tsx ├── README.md ├── postcss.config.js ├── .storybook │ ├── preview.ts │ └── main.ts ├── tsconfig.json ├── tailwind.config.ts ├── vite.config.ts ├── index.html └── package.json ├── packages ├── frameworks │ └── solid-vite │ │ ├── src │ │ ├── index.ts │ │ ├── plugins │ │ │ └── solid-docgen.ts │ │ ├── types.ts │ │ └── preset.ts │ │ ├── preset.js │ │ ├── README.md │ │ ├── tsconfig.json │ │ └── package.json └── renderers │ └── solid │ ├── src │ ├── index.ts │ ├── globals.ts │ ├── typings.d.ts │ ├── entry-preview.tsx │ ├── entry-preview-docs.tsx │ ├── types.ts │ ├── render.tsx │ ├── preset.ts │ ├── portable-stories.tsx │ ├── public-types.ts │ ├── renderToCanvas.tsx │ └── docs │ │ ├── sourceDecorator.test.tsx │ │ └── sourceDecorator.tsx │ ├── preset.js │ ├── README.md │ ├── tsconfig.json │ ├── template │ └── cli │ │ ├── js │ │ ├── Header.stories.js │ │ ├── Button.jsx │ │ ├── Page.stories.js │ │ ├── Button.stories.jsx │ │ ├── Header.jsx │ │ └── Page.jsx │ │ └── ts │ │ ├── Header.stories.ts │ │ ├── Page.stories.ts │ │ ├── Button.tsx │ │ ├── Button.stories.ts │ │ ├── Header.tsx │ │ └── Page.tsx │ └── package.json ├── .yarnrc.yml ├── .editorconfig ├── .gitignore ├── eslint.config.js ├── .github ├── workflows │ ├── ci.yml │ ├── solid-vite.yml │ └── solid.yml └── actions │ └── setup │ └── action.yml ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json ├── scripts ├── utils │ └── exec.ts └── prepare │ ├── check.ts │ └── build.ts └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.13.1 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": true } 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *storybook.log -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /example/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storybookjs/solidjs/HEAD/example/src/assets/favicon.ico -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Example SolidJS App with Storybook 2 | 3 | Start the app with `yarn dev` and Storybook with `yarn storybook`. 4 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/index.ts: -------------------------------------------------------------------------------- 1 | import './globals'; 2 | 3 | export * from './portable-stories'; 4 | export * from './public-types'; 5 | -------------------------------------------------------------------------------- /packages/renderers/solid/preset.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | module.exports = require('./dist/preset'); 3 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/preset.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | module.exports = require('./dist/preset'); 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | 6 | [*.{js,jsx,json,yml,ts,tsx,vue,svelte,html}] 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/globals.ts: -------------------------------------------------------------------------------- 1 | import { global } from '@storybook/global'; 2 | 3 | const { window: globalWindow } = global; 4 | 5 | globalWindow.STORYBOOK_ENV = 'solid'; 6 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/README.md: -------------------------------------------------------------------------------- 1 | # Storybook for SolidJS 2 | 3 | See the [Storybook Docs](https://storybook.js.org/docs?renderer=solid) for the best documentation on getting started with Storybook. 4 | -------------------------------------------------------------------------------- /packages/renderers/solid/README.md: -------------------------------------------------------------------------------- 1 | # Storybook SolidJS Renderer 2 | 3 | See the [Storybook Docs](https://storybook.js.org/docs?renderer=solid) for the best documentation on getting started with Storybook. 4 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | declare var STORYBOOK_ENV: 'solid'; 4 | declare var FRAMEWORK_OPTIONS: any; 5 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /example/src/components/SolidLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import solidLogo from './solid-logo.svg'; 2 | 3 | export function SolidLogo() { 4 | return ( 5 | Solid logo 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /packages/renderers/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "storybook-solidjs": ["./src"] 7 | } 8 | }, 9 | "include": ["src/**/*", "template/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /example/src/components/Logos/plus.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/src/components/StorybookLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import storybookLogo from './storybook-logo.svg'; 2 | 3 | export function StorybookLogo() { 4 | return ( 5 | Storybook logo 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /example/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '../src/index.css'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | *.DS_Store 4 | 5 | # yarn (no zero-install) 6 | /**/.pnp.* 7 | /**/.yarn/* 8 | !/**/.yarn/patches 9 | !/**/.yarn/plugins 10 | !/**/.yarn/releases 11 | !/**/.yarn/sdks 12 | !/**/.yarn/versions 13 | 14 | # build 15 | dist 16 | bench 17 | 18 | # vite 19 | vite.config.ts.timestamp-* 20 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "storybook-solidjs": ["../packages/renderers/solid"], 7 | "storybook-solidjs-vite": ["../packages/frameworks/solid-vite"] 8 | }, 9 | "types": ["vite/client"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './index.html', 6 | './src/**/*.{js,ts,jsx,tsx,css,md,mdx,html,json,scss}', 7 | ], 8 | darkMode: 'class', 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /example/src/components/Logos/index.tsx: -------------------------------------------------------------------------------- 1 | import { SolidLogo } from '../SolidLogo'; 2 | import { StorybookLogo } from '../StorybookLogo'; 3 | 4 | import plusIcon from './plus.svg'; 5 | 6 | export function Logos() { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /example/src/components/Logos/index.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | import { Logos } from '.'; 3 | 4 | const meta: Meta = { 5 | component: Logos, 6 | parameters: { 7 | layout: 'centered', 8 | backgrounds: { default: 'dark' }, 9 | }, 10 | }; 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/entry-preview.tsx: -------------------------------------------------------------------------------- 1 | /* Configuration for default renderer. */ 2 | 3 | import { Decorator } from './public-types'; 4 | import { solidReactivityDecorator } from './renderToCanvas'; 5 | 6 | export const parameters = { renderer: 'solid' }; 7 | export { render } from './render'; 8 | export { renderToCanvas } from './renderToCanvas'; 9 | 10 | export const decorators: Decorator[] = [solidReactivityDecorator]; 11 | -------------------------------------------------------------------------------- /example/src/components/SolidLogo/index.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | import { SolidLogo } from '.'; 3 | 4 | const meta: Meta = { 5 | component: SolidLogo, 6 | parameters: { 7 | layout: 'centered', 8 | backgrounds: { default: 'dark' }, 9 | }, 10 | }; 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /example/src/components/StorybookLogo/index.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | import { StorybookLogo } from '.'; 3 | 4 | const meta: Meta = { 5 | component: StorybookLogo, 6 | parameters: { 7 | layout: 'centered', 8 | backgrounds: { default: 'dark' }, 9 | }, 10 | }; 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from 'solid-js/web'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | const root = document.getElementById('root'); 8 | 9 | if (import.meta.env.DEV && !(root instanceof HTMLElement)) { 10 | throw new Error( 11 | 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?', 12 | ); 13 | } 14 | 15 | render(() => , root!); 16 | -------------------------------------------------------------------------------- /example/src/App.module.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .header { 6 | background-color: #282c34; 7 | min-height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: calc(10px + 2vmin); 13 | color: white; 14 | gap: 1em; 15 | } 16 | 17 | @keyframes logo-spin { 18 | from { 19 | transform: rotate(0deg); 20 | } 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: 8 | -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 9 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: 16 | source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 17 | } 18 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | // import devtools from 'solid-devtools/vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | /* 8 | Uncomment the following line to enable solid-devtools. 9 | For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme 10 | */ 11 | // devtools(), 12 | solidPlugin(), 13 | ], 14 | server: { 15 | port: 3000, 16 | }, 17 | build: { 18 | target: 'esnext', 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import * as depend from 'eslint-plugin-depend'; 5 | 6 | export default [ 7 | { 8 | rules: { 9 | 'depend/ban-dependencies': [ 10 | 'error', 11 | { 12 | allowed: ['fs-extra'], 13 | }, 14 | ], 15 | }, 16 | }, 17 | { ignores: ['**/dist/'] }, 18 | { files: ['**/*.{ts,tsx}'] }, 19 | { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, 20 | depend.configs['flat/recommended'], 21 | pluginJs.configs.recommended, 22 | ...tseslint.configs.recommended, 23 | ]; 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | merge_group: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | jobs: 9 | format: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/setup 14 | - name: Run formatter 15 | run: yarn check:format 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ./.github/actions/setup 21 | - name: Run linter 22 | run: yarn check:lint 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: ./.github/actions/setup 28 | - name: Run tests 29 | run: yarn check:test 30 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { Component } from 'solid-js'; 2 | 3 | import styles from './App.module.css'; 4 | import { Logos } from './components/Logos'; 5 | import { Link } from './components/Link'; 6 | 7 | const App: Component = () => { 8 | return ( 9 |
10 |
11 | 12 |

13 | You're using storybook-solidjs! 14 |

15 | 21 | Learn Solid 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/entry-preview-docs.tsx: -------------------------------------------------------------------------------- 1 | /* Configuration for doc-mode renderer (`storybook dev --docs`). */ 2 | 3 | import { 4 | enhanceArgTypes, 5 | extractComponentDescription, 6 | } from '@storybook/docs-tools'; 7 | import { sourceDecorator } from './docs/sourceDecorator'; 8 | import type { Decorator, SolidRenderer } from './public-types'; 9 | import { ArgTypesEnhancer } from '@storybook/types'; 10 | 11 | export const parameters = { 12 | docs: { 13 | story: { inline: true }, 14 | extractComponentDescription, //TODO solid-docgen plugin needs to be created. 15 | }, 16 | }; 17 | 18 | export const decorators: Decorator[] = [sourceDecorator]; 19 | 20 | export const argTypesEnhancers: ArgTypesEnhancer[] = [ 21 | enhanceArgTypes, 22 | ]; 23 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Component, JSXElement } from 'solid-js'; 2 | import type { Args, WebRenderer } from '@storybook/types'; 3 | 4 | export type { RenderContext } from '@storybook/types'; 5 | export type { StoryContext } from '@storybook/types'; 6 | 7 | export interface SolidRenderer extends WebRenderer { 8 | // @ts-expect-error Component expects a record, but TArgs are unknown. 9 | component: Component; 10 | storyResult: StoryFnSolidReturnType; 11 | } 12 | 13 | export interface ShowErrorArgs { 14 | title: string; 15 | description: string; 16 | } 17 | 18 | export type StoryFnSolidReturnType = JSXElement; 19 | export type ComponentsData = { 20 | [key: string]: { args: Args; rendered?: boolean; disposeFn?: () => void }; 21 | }; 22 | -------------------------------------------------------------------------------- /example/src/components/Link/index.css: -------------------------------------------------------------------------------- 1 | .storybook-link { 2 | display: inline-block; 3 | cursor: pointer; 4 | border: 0; 5 | border-radius: 3em; 6 | font-weight: 700; 7 | line-height: 1; 8 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | } 10 | .storybook-link--primary { 11 | background-color: #1ea7fd; 12 | color: white; 13 | } 14 | .storybook-link--secondary { 15 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 16 | background-color: transparent; 17 | color: #333; 18 | } 19 | .storybook-link--small { 20 | padding: 10px 16px; 21 | font-size: 12px; 22 | } 23 | .storybook-link--medium { 24 | padding: 11px 20px; 25 | font-size: 14px; 26 | } 27 | .storybook-link--large { 28 | padding: 12px 24px; 29 | font-size: 16px; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": ".", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "ignoreDeprecations": "5.0", 9 | "incremental": false, 10 | "isolatedModules": true, 11 | "jsx": "preserve", 12 | "jsxImportSource": "solid-js", 13 | "lib": ["dom", "dom.iterable", "esnext"], 14 | "module": "ES2022", 15 | "moduleResolution": "Bundler", 16 | "noImplicitAny": true, 17 | "noUnusedLocals": false, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "noEmit": true, 21 | "strictBindCallApply": true, 22 | "target": "ES2022" 23 | }, 24 | "exclude": ["dist", "**/dist", "node_modules", "**/node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/js/Header.stories.js: -------------------------------------------------------------------------------- 1 | import { fn } from '@storybook/test'; 2 | 3 | import { Header } from './Header'; 4 | 5 | export default { 6 | title: 'Example/Header', 7 | component: Header, 8 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/solid/writing-docs/docs-page 9 | tags: ['autodocs'], 10 | parameters: { 11 | // More on how to position stories at: https://storybook.js.org/docs/solid/configure/story-layout 12 | layout: 'fullscreen', 13 | }, 14 | args: { 15 | onLogin: fn(), 16 | onLogout: fn(), 17 | onCreateAccount: fn(), 18 | }, 19 | }; 20 | 21 | export const LoggedIn = { 22 | args: { 23 | user: { 24 | name: 'Jane Doe', 25 | }, 26 | }, 27 | }; 28 | 29 | export const LoggedOut = {}; 30 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/src/plugins/solid-docgen.ts: -------------------------------------------------------------------------------- 1 | import { PluginOption } from 'vite'; 2 | import MagicString from 'magic-string'; 3 | 4 | export function solidDocgen(): PluginOption { 5 | return { 6 | enforce: 'pre', 7 | name: 'solid-docgen-plugin', 8 | async transform(src: string, id: string) { 9 | if (id.match(/(node_modules|\.stories\.)/gi)) return undefined; 10 | 11 | //Solid Docgen will be only generated for tsx, jsx files. 12 | if (id.match(/\.(tsx|jsx)$/)) { 13 | const s = new MagicString(src); 14 | 15 | //TODO: needs more research if a solid doc generator is needed for extracting args descriptions. 16 | 17 | return { 18 | code: s.toString(), 19 | map: s.generateMap({ hires: true, source: id }), 20 | }; 21 | } 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/render.tsx: -------------------------------------------------------------------------------- 1 | import { ArgsStoryFn } from '@storybook/types'; 2 | import { SolidRenderer } from './types'; 3 | 4 | /** 5 | * Default render function for a story definition (inside a csf file) without 6 | * a render function. e.g: 7 | * ```typescript 8 | * export const StoryExample = { 9 | * args: { 10 | * disabled: true, 11 | * children: "Hello World", 12 | * }, 13 | * }; 14 | * ``` 15 | */ 16 | export const render: ArgsStoryFn = (_, context) => { 17 | const { id, component: Component } = context; 18 | 19 | if (!Component) { 20 | throw new Error( 21 | `Unable to render story ${id} as the component annotation is missing from the default export`, 22 | ); 23 | } 24 | 25 | // context.args is a SolidJS proxy thanks to the solidReactivityDecorator. 26 | return ; 27 | }; 28 | -------------------------------------------------------------------------------- /example/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from 'storybook-solidjs-vite'; 2 | 3 | import { join, dirname } from 'path'; 4 | 5 | /** 6 | * This function is used to resolve the absolute path of a package. 7 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 8 | */ 9 | function getAbsolutePath(value: string): string { 10 | return dirname(require.resolve(join(value, 'package.json'))); 11 | } 12 | const config: StorybookConfig = { 13 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 14 | addons: [ 15 | getAbsolutePath('@storybook/addon-links'), 16 | getAbsolutePath('@storybook/addon-essentials'), 17 | getAbsolutePath('@chromatic-com/storybook'), 18 | getAbsolutePath('@storybook/addon-interactions'), 19 | ], 20 | framework: { 21 | name: getAbsolutePath('storybook-solidjs-vite'), 22 | options: {}, 23 | }, 24 | }; 25 | export default config; 26 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/ts/Header.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import { Header } from './Header'; 6 | 7 | const meta = { 8 | title: 'Example/Header', 9 | component: Header, 10 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/solid/writing-docs/docs-page 11 | tags: ['autodocs'], 12 | parameters: { 13 | // More on how to position stories at: https://storybook.js.org/docs/solid/configure/story-layout 14 | layout: 'fullscreen', 15 | }, 16 | args: { 17 | onLogin: fn(), 18 | onLogout: fn(), 19 | onCreateAccount: fn(), 20 | }, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const LoggedIn: Story = { 27 | args: { 28 | user: { 29 | name: 'Jane Doe', 30 | }, 31 | }, 32 | }; 33 | 34 | export const LoggedOut: Story = {}; 35 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Standard Workflow 2 | runs: 3 | using: composite 4 | steps: 5 | - uses: actions/setup-node@v4 6 | with: 7 | node-version: '20' 8 | - name: Enable Corepack 9 | shell: bash 10 | run: corepack enable && corepack install 11 | - name: Print versions 12 | shell: bash 13 | run: node --version && yarn --version 14 | - name: Get yarn cache directory path 15 | id: yarn-cache-dir-path 16 | shell: bash 17 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 18 | - uses: actions/cache@v4 19 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 20 | with: 21 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 22 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-yarn- 25 | - name: Install project dependencies 26 | shell: bash 27 | run: yarn install 28 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/js/Button.jsx: -------------------------------------------------------------------------------- 1 | import { mergeProps, splitProps } from 'solid-js'; 2 | import './button.css'; 3 | 4 | /** 5 | * Primary UI component for user interaction 6 | */ 7 | export const Button = (props) => { 8 | props = mergeProps({ size: 'small' }, props); 9 | const [local, rest] = splitProps(props, [ 10 | 'primary', 11 | 'size', 12 | 'backgroundColor', 13 | 'label', 14 | ]); 15 | 16 | return ( 17 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /.github/workflows/solid-vite.yml: -------------------------------------------------------------------------------- 1 | name: Solid Vite Pipeline 2 | on: 3 | workflow_call: 4 | inputs: 5 | publish: 6 | description: Publish? 7 | required: false 8 | type: boolean 9 | default: false 10 | workflow_dispatch: 11 | inputs: 12 | publish: 13 | description: Publish? 14 | required: false 15 | type: boolean 16 | default: false 17 | defaults: 18 | run: 19 | working-directory: packages/frameworks/solid-vite 20 | jobs: 21 | solid-vite-workflow: 22 | runs-on: ubuntu-latest 23 | name: Lib workflow 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: ./.github/actions/setup 27 | - name: Check bundling 28 | run: yarn check 29 | working-directory: packages/frameworks/solid-vite 30 | - name: Publish to NPM 31 | if: inputs.publish 32 | run: yarn npm publish 33 | working-directory: packages/frameworks/solid-vite 34 | env: 35 | YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/js/Page.stories.js: -------------------------------------------------------------------------------- 1 | import { expect, userEvent, within } from '@storybook/test'; 2 | 3 | import { Page } from './Page'; 4 | 5 | export default { 6 | title: 'Example/Page', 7 | component: Page, 8 | parameters: { 9 | // More on how to position stories at: https://storybook.js.org/docs/solid/configure/story-layout 10 | layout: 'fullscreen', 11 | }, 12 | }; 13 | 14 | export const LoggedOut = {}; 15 | 16 | // More on interaction testing: https://storybook.js.org/docs/solid/writing-tests/interaction-testing 17 | export const LoggedIn = { 18 | play: async ({ canvasElement }) => { 19 | const canvas = within(canvasElement); 20 | const loginButton = canvas.getByRole('button', { name: /Log in/i }); 21 | await expect(loginButton).toBeInTheDocument(); 22 | await userEvent.click(loginButton); 23 | await expect(loginButton).not.toBeInTheDocument(); 24 | 25 | const logoutButton = canvas.getByRole('button', { name: /Log out/i }); 26 | await expect(logoutButton).toBeInTheDocument(); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-solid-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "", 6 | "type": "module", 7 | "scripts": { 8 | "start": "vite", 9 | "dev": "vite", 10 | "build": "vite build", 11 | "serve": "vite preview", 12 | "storybook": "storybook dev -p 6006", 13 | "build-storybook": "storybook build" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@chromatic-com/storybook": "^1.9.0", 18 | "@storybook/addon-essentials": "next", 19 | "@storybook/addon-interactions": "next", 20 | "@storybook/addon-links": "next", 21 | "@storybook/test": "next", 22 | "autoprefixer": "^10.4.20", 23 | "postcss": "^8.4.47", 24 | "solid-devtools": "^0.33.0", 25 | "storybook": "next", 26 | "storybook-solidjs": "workspace:*", 27 | "storybook-solidjs-vite": "workspace:*", 28 | "tailwindcss": "^3.4.14", 29 | "typescript": "5.7.3", 30 | "vite": "^6.0.0", 31 | "vite-plugin-solid": "^2.8.2" 32 | }, 33 | "dependencies": { 34 | "solid-js": "^1.9.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig as StorybookConfigBase } from '@storybook/types'; 2 | import type { 3 | StorybookConfigVite, 4 | BuilderOptions, 5 | } from '@storybook/builder-vite'; 6 | 7 | type FrameworkName = 'storybook-solidjs-vite'; 8 | type BuilderName = '@storybook/builder-vite'; 9 | 10 | export type FrameworkOptions = { 11 | builder?: BuilderOptions; 12 | }; 13 | 14 | type StorybookConfigFramework = { 15 | framework: 16 | | FrameworkName 17 | | { 18 | name: FrameworkName; 19 | options: FrameworkOptions; 20 | }; 21 | core?: StorybookConfigBase['core'] & { 22 | builder?: 23 | | BuilderName 24 | | { 25 | name: BuilderName; 26 | options: BuilderOptions; 27 | }; 28 | }; 29 | }; 30 | 31 | /** 32 | * The interface for Storybook configuration in `main.ts` files. 33 | */ 34 | export type StorybookConfig = Omit< 35 | StorybookConfigBase, 36 | keyof StorybookConfigVite | keyof StorybookConfigFramework 37 | > & 38 | StorybookConfigVite & 39 | StorybookConfigFramework; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Storybook 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/preset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A preset is a configuration that enables developers to quickly set up and 3 | * customize their environment with a specific set of features, functionalities, or integrations. 4 | * 5 | * @see https://storybook.js.org/docs/addons/writing-presets 6 | * @see https://storybook.js.org/docs/api/main-config/main-config 7 | */ 8 | 9 | import { join } from 'node:path'; 10 | 11 | import type { PresetProperty } from '@storybook/types'; 12 | 13 | /** 14 | * Add additional scripts to run in the story preview. 15 | * 16 | * @see https://storybook.js.org/docs/api/main-config/main-config-preview-annotations 17 | */ 18 | export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( 19 | input = [], 20 | options, 21 | ) => { 22 | const docsConfig = await options.presets.apply('docs', {}, options); 23 | const docsEnabled = Object.keys(docsConfig).length > 0; 24 | const result: string[] = []; 25 | 26 | return result 27 | .concat(input) 28 | .concat([join(__dirname, 'entry-preview.mjs')]) 29 | .concat(docsEnabled ? [join(__dirname, 'entry-preview-docs.mjs')] : []); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/ts/Page.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | import { expect, userEvent, within } from '@storybook/test'; 3 | 4 | import { Page } from './Page'; 5 | 6 | const meta = { 7 | title: 'Example/Page', 8 | component: Page, 9 | parameters: { 10 | // More on how to position stories at: https://storybook.js.org/docs/solid/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const LoggedOut: Story = {}; 19 | 20 | // More on interaction testing: https://storybook.js.org/docs/solid/writing-tests/interaction-testing 21 | export const LoggedIn: Story = { 22 | play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { 23 | const canvas = within(canvasElement); 24 | const loginButton = canvas.getByRole('button', { name: /Log in/i }); 25 | await expect(loginButton).toBeInTheDocument(); 26 | await userEvent.click(loginButton); 27 | await expect(loginButton).not.toBeInTheDocument(); 28 | 29 | const logoutButton = canvas.getByRole('button', { name: /Log out/i }); 30 | await expect(logoutButton).toBeInTheDocument(); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /example/src/components/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, JSX, mergeProps, splitProps } from 'solid-js'; 2 | import './index.css'; 3 | 4 | export type LinkProps = { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large'; 17 | } & JSX.AnchorHTMLAttributes; 18 | 19 | /** 20 | * Primary UI component for user interaction 21 | */ 22 | export const Link: Component = (props) => { 23 | props = mergeProps({ size: 'small' as LinkProps['size'] }, props); 24 | const [local, rest] = splitProps(props, [ 25 | 'primary', 26 | 'size', 27 | 'backgroundColor', 28 | ]); 29 | 30 | return ( 31 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /.github/workflows/solid.yml: -------------------------------------------------------------------------------- 1 | name: Solid Renderer Pipeline 2 | on: 3 | pull_request: 4 | branches: 5 | - '*' 6 | paths: 7 | - packages/renderers/solid/** 8 | push: 9 | branches: 10 | - main 11 | paths: 12 | - packages/renderers/solid/** 13 | release: 14 | types: [published] 15 | workflow_dispatch: 16 | inputs: 17 | publish: 18 | description: Publish? 19 | required: false 20 | type: boolean 21 | default: false 22 | defaults: 23 | run: 24 | working-directory: packages/renderers/solid 25 | jobs: 26 | solid-workflow: 27 | runs-on: ubuntu-latest 28 | name: Lib workflow 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ./.github/actions/setup 32 | - name: Check bundling 33 | run: yarn check 34 | working-directory: packages/renderers/solid 35 | - name: Publish to NPM 36 | if: github.event_name == 'release' || inputs.publish 37 | run: yarn npm publish 38 | working-directory: packages/renderers/solid 39 | env: 40 | YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | solid-vite-workflow: 42 | needs: solid-workflow 43 | uses: ./.github/workflows/solid-vite.yml 44 | with: 45 | publish: ${{ (github.event_name == 'release') || (inputs.publish == true) }} 46 | secrets: inherit 47 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/js/Button.stories.jsx: -------------------------------------------------------------------------------- 1 | import { fn } from '@storybook/test'; 2 | 3 | import { Button } from './Button'; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/7.0/solid/writing-stories/introduction 6 | export default { 7 | title: 'Example/Button', 8 | component: Button, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 11 | layout: 'centered', 12 | }, 13 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 14 | tags: ['autodocs'], 15 | // More on argTypes: https://storybook.js.org/docs/api/argtypes 16 | argTypes: { 17 | backgroundColor: { control: 'color' }, 18 | }, 19 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 20 | args: { onClick: fn() }, 21 | }; 22 | 23 | // More on writing stories with args: https://storybook.js.org/docs/solid/writing-stories/args 24 | export const Primary = { 25 | args: { 26 | primary: true, 27 | label: 'Button', 28 | }, 29 | }; 30 | 31 | export const Secondary = { 32 | args: { 33 | label: 'Button', 34 | }, 35 | }; 36 | 37 | export const Large = { 38 | args: { 39 | size: 'large', 40 | label: 'Button', 41 | }, 42 | }; 43 | 44 | export const Small = { 45 | args: { 46 | size: 'small', 47 | label: 'Button', 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > NOTE: We are transitioning this repo to SolidJS community maintenance at 2 | > 3 | > https://github.com/solidjs-community/storybook 4 | 5 | # Storybook SolidJS 6 | 7 | This is a framework to allow using [Storybook](https://storybook.js.org/) with [SolidJS](https://www.solidjs.com/). 8 | 9 | ## Features 10 | 11 | - [x] `JS` and `TS` integration with Storybook CLI 12 | - [x] Fine grained updates in storybook controls 13 | - [x] Compatible with `@storybook/addon-interactions` 14 | - [x] Compatible with `@storybook/test` 15 | - [x] Code snippets generation in docs view mode 16 | - [ ] Automatic story actions 17 | - [ ] Full props table with description in docs view mode 18 | - [ ] `SolidJS` docs in the official Storybook website 19 | 20 | ## Notes about pending features ⏳ 21 | 22 | - **Automatic story actions**: Feature under research. For now actions can be implemented manually by using the `@storybook/addon-actions` API. 23 | 24 | - **Full props table with description in docs view mode**: Feature under research. For now, props are rendered partially in the docs view table with a blank description. 25 | 26 | - **`SolidJS` docs in the official Storybook website**: It's still pending to add documentation about Storybook stories definitions using SolidJS. 27 | 28 | ## Setup 29 | 30 | In an existing Solid project, run `npx storybook@latest init` (Storybook 8+ is required) 31 | 32 | See the [Storybook Docs](https://storybook.js.org/docs?renderer=solid) for the best documentation on getting started with Storybook. 33 | 34 | ## License 35 | 36 | [MIT License](./LICENSE) 37 | -------------------------------------------------------------------------------- /example/src/components/SolidLogo/solid-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/ts/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Component, mergeProps, splitProps } from 'solid-js'; 2 | import './button.css'; 3 | 4 | export interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large'; 17 | /** 18 | * Button contents 19 | */ 20 | label: string; 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void; 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button: Component = (props) => { 31 | props = mergeProps({ size: 'small' as ButtonProps['size'] }, props); 32 | const [local, rest] = splitProps(props, [ 33 | 'primary', 34 | 'size', 35 | 'backgroundColor', 36 | 'label', 37 | ]); 38 | 39 | return ( 40 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/portable-stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | setProjectAnnotations as originalSetProjectAnnotations, 4 | setDefaultProjectAnnotations, 5 | } from '@storybook/preview-api'; 6 | import type { 7 | NamedOrDefaultProjectAnnotations, 8 | NormalizedProjectAnnotations, 9 | } from '@storybook/types'; 10 | import type { SolidRenderer } from './types'; 11 | 12 | import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './entry-preview'; 13 | 14 | /** 15 | * Function that sets the globalConfig of your storybook. The global config is the preview module of 16 | * your .storybook folder. 17 | * 18 | * It should be run a single time, so that your global config (e.g. decorators) is applied to your 19 | * stories when using `composeStories` or `composeStory`. 20 | * 21 | * Example: 22 | * 23 | * ```jsx 24 | * // setup-file.js 25 | * import { setProjectAnnotations } from 'storybook-solidjs'; 26 | * import projectAnnotations from './.storybook/preview'; 27 | * 28 | * setProjectAnnotations(projectAnnotations); 29 | * ``` 30 | * 31 | * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview') 32 | */ 33 | export function setProjectAnnotations( 34 | projectAnnotations: 35 | | NamedOrDefaultProjectAnnotations 36 | | NamedOrDefaultProjectAnnotations[], 37 | ): NormalizedProjectAnnotations { 38 | setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS); 39 | return originalSetProjectAnnotations( 40 | projectAnnotations, 41 | ) as NormalizedProjectAnnotations; 42 | } 43 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/js/Header.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from './Button'; 2 | import './header.css'; 3 | 4 | export const Header = (props) => ( 5 |
6 |
7 |
8 | 14 | 15 | 19 | 23 | 27 | 28 | 29 |

Acme

30 |
31 |
32 | {props.user ? ( 33 | <> 34 | 35 | Welcome, {props.user.name}! 36 | 37 |
51 |
52 |
53 | ); 54 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/ts/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import { Button } from './Button'; 6 | 7 | // More on how to set up stories at: https://storybook.js.org/docs/7.0/solid/writing-stories/introduction 8 | const meta = { 9 | title: 'Example/Button', 10 | component: Button, 11 | parameters: { 12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 13 | layout: 'centered', 14 | }, 15 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 16 | tags: ['autodocs'], 17 | // More on argTypes: https://storybook.js.org/docs/api/argtypes 18 | argTypes: { 19 | backgroundColor: { control: 'color' }, 20 | }, 21 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 22 | args: { onClick: fn() }, 23 | } satisfies Meta; 24 | 25 | export default meta; 26 | type Story = StoryObj; 27 | 28 | // More on writing stories with args: https://storybook.js.org/docs/solid/writing-stories/args 29 | export const Primary: Story = { 30 | args: { 31 | primary: true, 32 | label: 'Button', 33 | }, 34 | }; 35 | 36 | export const Secondary: Story = { 37 | args: { 38 | label: 'Button', 39 | }, 40 | }; 41 | 42 | export const Large: Story = { 43 | args: { 44 | size: 'large', 45 | label: 'Button', 46 | }, 47 | }; 48 | 49 | export const Small: Story = { 50 | args: { 51 | size: 'small', 52 | label: 'Button', 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storybook-solidjs/root", 3 | "private": true, 4 | "type": "module", 5 | "workspaces": { 6 | "packages": [ 7 | "example", 8 | "packages/frameworks/*", 9 | "packages/renderers/*" 10 | ] 11 | }, 12 | "devDependencies": { 13 | "@eslint/js": "^9.13.0", 14 | "@fal-works/esbuild-plugin-global-externals": "^2.1.2", 15 | "@types/fs-extra": "^11.0.4", 16 | "@types/node": "^18.0.0", 17 | "esbuild": "^0.24.0", 18 | "esbuild-plugin-alias": "^0.2.1", 19 | "esbuild-plugin-solid": "^0.6.0", 20 | "eslint": "^9.13.0", 21 | "eslint-plugin-depend": "^0.11.0", 22 | "execa": "^9.4.0", 23 | "fdir": "^6.4.2", 24 | "fs-extra": "^11.2.0", 25 | "globals": "^15.11.0", 26 | "jiti": "^2.3.3", 27 | "picomatch": "^4.0.2", 28 | "prettier": "^3.3.3", 29 | "pretty-hrtime": "^1.0.3", 30 | "rollup": "^4.24.0", 31 | "rollup-plugin-dts": "^6.1.1", 32 | "slash": "^5.1.0", 33 | "solid-js": "^1.9.3", 34 | "sort-package-json": "^2.10.1", 35 | "ts-node": "^10.9.2", 36 | "tsup": "^8.4.0", 37 | "type-fest": "^4.35.0", 38 | "typescript": "5.7.3", 39 | "typescript-eslint": "^8.25.0", 40 | "vitest": "^2.1.4" 41 | }, 42 | "scripts": { 43 | "build": "yarn workspaces foreach --all -pt run build", 44 | "check:all": "yarn run check:lint && yarn run check:test && yarn run check:format", 45 | "check:format": "prettier --check .", 46 | "check:lint": "eslint .", 47 | "check:test": "vitest run --no-isolate", 48 | "fix:all": "yarn run fix:lint && yarn run fix:format", 49 | "fix:format": "prettier --write .", 50 | "fix:lint": "eslint . --fix", 51 | "test": "vitest" 52 | }, 53 | "packageManager": "yarn@4.5.1" 54 | } 55 | -------------------------------------------------------------------------------- /example/src/components/Link/index.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from 'storybook-solidjs'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import { Link } from '.'; 6 | 7 | // More on how to set up stories at: https://storybook.js.org/docs/7.0/solid/writing-stories/introduction 8 | const meta = { 9 | title: 'Example/Link', 10 | component: Link, 11 | parameters: { 12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 13 | layout: 'centered', 14 | }, 15 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 16 | tags: ['autodocs'], 17 | // More on argTypes: https://storybook.js.org/docs/api/argtypes 18 | argTypes: { 19 | backgroundColor: { control: 'color' }, 20 | }, 21 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 22 | args: { onClick: fn() }, 23 | } satisfies Meta; 24 | 25 | export default meta; 26 | type Story = StoryObj; 27 | 28 | // More on writing stories with args: https://storybook.js.org/docs/solid/writing-stories/args 29 | export const Primary: Story = { 30 | args: { 31 | primary: true, 32 | href: '#', 33 | children: 'Link', 34 | }, 35 | }; 36 | 37 | export const Secondary: Story = { 38 | args: { 39 | href: '#', 40 | children: 'Link', 41 | }, 42 | }; 43 | 44 | export const Large: Story = { 45 | args: { 46 | size: 'large', 47 | href: '#', 48 | children: 'Link', 49 | }, 50 | }; 51 | 52 | export const Small: Story = { 53 | args: { 54 | size: 'small', 55 | href: '#', 56 | children: 'Link', 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /scripts/utils/exec.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line depend/ban-dependencies 2 | import { type Options, type ResultPromise, execa } from 'execa'; 3 | import picocolors from 'picocolors'; 4 | 5 | const logger = console; 6 | 7 | type StepOptions = { 8 | startMessage?: string; 9 | errorMessage?: string; 10 | dryRun?: boolean; 11 | debug?: boolean; 12 | cancelSignal?: AbortSignal; 13 | }; 14 | 15 | export const exec = async ( 16 | command: string | string[], 17 | options: Options = {}, 18 | { startMessage, errorMessage, dryRun, debug, cancelSignal }: StepOptions = {}, 19 | ): Promise => { 20 | logger.info(); 21 | 22 | if (startMessage) { 23 | logger.info(startMessage); 24 | } 25 | 26 | if (dryRun) { 27 | logger.info(`\n> ${command}\n`); 28 | return undefined; 29 | } 30 | 31 | const defaultOptions: Options = { 32 | shell: true, 33 | stdout: debug ? 'inherit' : 'pipe', 34 | stderr: debug ? 'inherit' : 'pipe', 35 | cancelSignal, 36 | }; 37 | let currentChild: ResultPromise; 38 | 39 | try { 40 | if (typeof command === 'string') { 41 | logger.debug(`> ${command}`); 42 | currentChild = execa(command, { ...defaultOptions, ...options }); 43 | await currentChild; 44 | } else { 45 | for (const subcommand of command) { 46 | logger.debug(`> ${subcommand}`); 47 | currentChild = execa(subcommand, { ...defaultOptions, ...options }); 48 | await currentChild; 49 | } 50 | } 51 | } catch (err) { 52 | if (!(typeof err === 'object' && 'killed' in err! && err.killed)) { 53 | logger.error( 54 | picocolors.red(`An error occurred while executing: \`${command}\``), 55 | ); 56 | logger.log(`${errorMessage}\n`); 57 | } 58 | 59 | throw err; 60 | } 61 | 62 | return undefined; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/src/preset.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path'; 2 | 3 | /** 4 | * A preset is a configuration that enables developers to quickly set up and 5 | * customize their environment with a specific set of features, functionalities, or integrations. 6 | * 7 | * @see https://storybook.js.org/docs/addons/writing-presets 8 | * @see https://storybook.js.org/docs/api/main-config/main-config 9 | */ 10 | 11 | import { hasVitePlugins } from '@storybook/builder-vite'; 12 | import type { PresetProperty } from '@storybook/types'; 13 | //import { solidDocgen } from './plugins/solid-docgen'; 14 | import type { StorybookConfig } from './types'; 15 | 16 | // Helper for getting the location of dependencies. 17 | const getAbsolutePath = (input: I): I => 18 | dirname(require.resolve(join(input, 'package.json'))) as I; 19 | 20 | /** 21 | * Configures Storybook's internal features. 22 | * 23 | * @see https://storybook.js.org/docs/api/main-config/main-config-core 24 | */ 25 | export const core: PresetProperty<'core', StorybookConfig> = { 26 | builder: getAbsolutePath('@storybook/builder-vite'), 27 | renderer: getAbsolutePath('storybook-solidjs'), 28 | }; 29 | 30 | /** 31 | * Customize Storybook's Vite setup when using the Vite builder. 32 | * 33 | * @see https://storybook.js.org/docs/api/main-config/main-config-vite-final 34 | */ 35 | export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { 36 | const { plugins = [] } = config; 37 | 38 | // Add solid plugin if not present 39 | if (!(await hasVitePlugins(plugins, ['vite-plugin-solid']))) { 40 | const { default: solidPlugin } = await import('vite-plugin-solid'); 41 | plugins.push(solidPlugin()); 42 | 43 | // Docgen plugin is prioritized as first pluging to be loaded for having file raw code. 44 | // plugins.unshift(solidDocgen()); 45 | } 46 | 47 | return config; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/ts/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Button } from './Button'; 3 | import './header.css'; 4 | 5 | type User = { 6 | name: string; 7 | }; 8 | 9 | interface HeaderProps { 10 | user?: User; 11 | onLogin: () => void; 12 | onLogout: () => void; 13 | onCreateAccount: () => void; 14 | } 15 | 16 | export const Header: Component = (props) => ( 17 |
18 |
19 |
20 | 26 | 27 | 31 | 35 | 39 | 40 | 41 |

Acme

42 |
43 |
44 | {props.user ? ( 45 | <> 46 | 47 | Welcome, {props.user.name}! 48 | 49 |
63 |
64 |
65 | ); 66 | -------------------------------------------------------------------------------- /packages/frameworks/solid-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-solidjs-vite", 3 | "version": "1.0.0-beta.7", 4 | "description": "Storybook for SolidJS and Vite: Develop SolidJS in isolation with Hot Reloading.", 5 | "keywords": [ 6 | "storybook" 7 | ], 8 | "homepage": "https://github.com/solidjs-community/storybook", 9 | "bugs": { 10 | "url": "https://github.com/solidjs-community/storybook/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/solidjs-community/storybook.git" 15 | }, 16 | "funding": { 17 | "type": "opencollective", 18 | "url": "https://opencollective.com/storybook" 19 | }, 20 | "license": "MIT", 21 | "exports": { 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "node": "./dist/index.js", 25 | "require": "./dist/index.js", 26 | "import": "./dist/index.mjs" 27 | }, 28 | "./preset": { 29 | "types": "./dist/preset.d.ts", 30 | "require": "./dist/preset.js", 31 | "import": "./dist/preset.mjs" 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "main": "dist/index.js", 36 | "module": "dist/index.mjs", 37 | "types": "dist/index.d.ts", 38 | "files": [ 39 | "dist/**/*", 40 | "template/**/*", 41 | "README.md", 42 | "*.js", 43 | "*.d.ts" 44 | ], 45 | "scripts": { 46 | "prepack": "yarn build --optimized --reset", 47 | "build": "npx jiti ../../../scripts/prepare/build.ts", 48 | "check": "npx jiti ../../../scripts/prepare/check.ts" 49 | }, 50 | "dependencies": { 51 | "@storybook/builder-vite": "next", 52 | "@storybook/types": "next", 53 | "magic-string": "^0.30.11", 54 | "storybook-solidjs": "workspace:*" 55 | }, 56 | "devDependencies": { 57 | "solid-js": "~1.9.0", 58 | "storybook": "next", 59 | "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" 60 | }, 61 | "peerDependencies": { 62 | "solid-js": "~1.9.0", 63 | "storybook": "*", 64 | "vite": "^4.0.0 || ^5.0.0 || ^6.0.0", 65 | "vite-plugin-solid": "^2.10.0" 66 | }, 67 | "engines": { 68 | "node": ">=18" 69 | }, 70 | "publishConfig": { 71 | "access": "public" 72 | }, 73 | "bundler": { 74 | "entries": [ 75 | "./src/index.ts", 76 | "./src/preset.ts" 77 | ], 78 | "platform": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/renderers/solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-solidjs", 3 | "version": "1.0.0-beta.7", 4 | "description": "Storybook SolidJS renderer", 5 | "keywords": [ 6 | "storybook" 7 | ], 8 | "homepage": "https://github.com/solidjs-community/storybook", 9 | "bugs": { 10 | "url": "https://github.com/solidjs-community/storybook/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/solidjs-community/storybook.git" 15 | }, 16 | "funding": { 17 | "type": "opencollective", 18 | "url": "https://opencollective.com/storybook" 19 | }, 20 | "license": "MIT", 21 | "exports": { 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "import": "./dist/index.mjs", 25 | "require": "./dist/index.js" 26 | }, 27 | "./preset": "./preset.js", 28 | "./dist/entry-preview.mjs": "./dist/entry-preview.mjs", 29 | "./dist/entry-preview-docs.mjs": "./dist/entry-preview-docs.mjs", 30 | "./package.json": "./package.json" 31 | }, 32 | "main": "dist/index.js", 33 | "module": "dist/index.mjs", 34 | "types": "dist/index.d.ts", 35 | "files": [ 36 | "dist/**/*", 37 | "template/**/*", 38 | "README.md", 39 | "*.js", 40 | "*.d.ts" 41 | ], 42 | "scripts": { 43 | "prepack": "yarn build --optimized --reset", 44 | "build": "npx jiti ../../../scripts/prepare/build.ts", 45 | "check": "npx jiti ../../../scripts/prepare/check.ts" 46 | }, 47 | "dependencies": { 48 | "@babel/standalone": "^7.26.2", 49 | "@storybook/docs-tools": "next", 50 | "@storybook/global": "^5.0.0", 51 | "@storybook/preview-api": "next", 52 | "@storybook/types": "next", 53 | "@types/babel__standalone": "link:.yarn/cache/null", 54 | "async-mutex": "^0.5.0" 55 | }, 56 | "devDependencies": { 57 | "storybook": "next" 58 | }, 59 | "peerDependencies": { 60 | "@storybook/test": "*", 61 | "solid-js": "~1.9.0", 62 | "storybook": "*" 63 | }, 64 | "peerDependenciesMeta": { 65 | "@storybook/test": { 66 | "optional": true 67 | } 68 | }, 69 | "engines": { 70 | "node": ">=18.0.0" 71 | }, 72 | "publishConfig": { 73 | "access": "public" 74 | }, 75 | "bundler": { 76 | "entries": [ 77 | "./src/index.ts", 78 | "./src/preset.ts", 79 | "./src/entry-preview.tsx", 80 | "./src/entry-preview-docs.tsx" 81 | ], 82 | "platform": "browser" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scripts/prepare/check.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import { join } from 'path'; 3 | import ts from 'typescript'; 4 | 5 | const run = async ({ cwd }: { cwd: string }) => { 6 | const { 7 | bundler: { tsConfig: tsconfigPath = 'tsconfig.json' }, 8 | } = await fs.readJson(join(cwd, 'package.json')); 9 | 10 | const { options, fileNames } = getTSFilesAndConfig(tsconfigPath); 11 | const { program, host } = getTSProgramAndHost(fileNames, options); 12 | 13 | const tsDiagnostics = getTSDiagnostics(program, cwd, host); 14 | if (tsDiagnostics.length > 0) { 15 | console.log(tsDiagnostics); 16 | process.exit(1); 17 | } else { 18 | console.log('no type errors'); 19 | } 20 | 21 | // TODO, add more package checks here, like: 22 | // - check for missing dependencies/peerDependencies 23 | // - check for unused exports 24 | 25 | if (process.env.CI !== 'true') { 26 | console.log('done'); 27 | } 28 | }; 29 | 30 | run({ cwd: process.cwd() }).catch((err: unknown) => { 31 | // We can't let the stack try to print, it crashes in a way that sets the exit code to 0. 32 | // Seems to have something to do with running JSON.parse() on binary / base64 encoded sourcemaps 33 | // in @cspotcode/source-map-support 34 | if (err instanceof Error) { 35 | console.error(err.message); 36 | } 37 | process.exit(1); 38 | }); 39 | 40 | function getTSDiagnostics( 41 | program: ts.Program, 42 | cwd: string, 43 | host: ts.CompilerHost, 44 | ) { 45 | return ts.formatDiagnosticsWithColorAndContext( 46 | ts 47 | .getPreEmitDiagnostics(program) 48 | .filter((d) => d.file?.fileName.startsWith(cwd)), 49 | host, 50 | ); 51 | } 52 | 53 | function getTSProgramAndHost(fileNames: string[], options: ts.CompilerOptions) { 54 | const program = ts.createProgram({ 55 | rootNames: fileNames, 56 | options: { 57 | module: ts.ModuleKind.CommonJS, 58 | ...options, 59 | declaration: false, 60 | noEmit: true, 61 | }, 62 | }); 63 | 64 | const host = ts.createCompilerHost(program.getCompilerOptions()); 65 | return { program, host }; 66 | } 67 | 68 | function getTSFilesAndConfig(tsconfigPath: string) { 69 | const content = ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile); 70 | return ts.parseJsonSourceFileConfigFileContent( 71 | content, 72 | { 73 | useCaseSensitiveFileNames: true, 74 | readDirectory: ts.sys.readDirectory, 75 | fileExists: ts.sys.fileExists, 76 | readFile: ts.sys.readFile, 77 | }, 78 | process.cwd(), 79 | { 80 | noEmit: true, 81 | outDir: join(process.cwd(), 'types'), 82 | target: ts.ScriptTarget.ES2022, 83 | declaration: false, 84 | }, 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | storybook-solidjs is developed against a specific node version which is defined in an `.nvmrc` file. You can use any Node version manager that uses the `.nvmrc` configuration file. 4 | 5 | ## Ensure you have the required system utilities 6 | 7 | You will need to have the following installed: 8 | 9 | - git 10 | - Node.js 11 | - Yarn 12 | 13 | For Node.js and Yarn, it is recommended to use a Node version manager to install Node, and then use corepack to install Yarn. 14 | 15 | ## Starting Development 16 | 17 | Install dependencies with `yarn`. 18 | 19 | To build packages, run `yarn build`, either in the top-level for all or inside a specific package for one. 20 | 21 | For formatting, linting, and testing, run `yarn check:all` to run checks and `yarn fix:all` to apply fixes where possible. 22 | There is also `*:format`, `*:lint`, and `*:test` variants for running them separately (e.g. `yarn check:format` to check formatting). 23 | (There is no `fix:test` variant) 24 | These checks run during CI, so remember to run `yarn fix:all` before pushing (or create a githook to do it automatically). 25 | 26 |
27 | A warning for developers on Windows 28 | I don't recommend developing this on Windows due to issues that appear when using yarn. 29 | I've seen yarn not apply the correct version of itself, have issues when installing dependencies, and calculate checksums differently (which becomes a problem with CI checks). 30 | If you don't have an alternative Linux or MacOS development environment, I would recommend using WSL exclusively when developing on Windows. 31 |
32 | 33 | ## Testing 34 | 35 | The example application can be used to test the framework. 36 | It has `yarn storybook` for testing Storybook and `yarn dev` for testing the app. 37 | 38 | For testing with external projects that use Yarn, the framework and renderer can be linked locally. 39 | 40 | **Note:** The default Yarn Plug n' Play installs WILL NOT work when testing. 41 | This is because Yarn PnP will use virtual paths for dependencies of linked dependencies. 42 | The framework does not resolve these virtual paths, so your test app will break. 43 | This can be fixed by specifying the node linker to be "node-modules". 44 | 45 | ### Example External Testing App 46 | 47 | 1. Create a SolidJS application using a template: `yarn dlx degit solidjs/templates/ts my-solid-app` and `cd my-solid-app` and `rm pnpm-lock.yaml`. 48 | 2. Initialize a linkable Storybook project: `yarn dlx storybook@latest init --linkable` 49 | 3. Create a `.yarnrc.yml` file with the following contents to avoid Yarn PnP: 50 | 51 | ```yml 52 | nodeLinker: node-modules 53 | ``` 54 | 55 | 4. Link to the framework and renderer with `yarn link ` (call twice with the absolute path to the framework "packages/frameworks/solid-vite" and renderer "packages/renderers/solid") 56 | 5. Install dependencies with `yarn` 57 | 6. Run Storybook with `yarn storybook` 58 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/js/Page.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js'; 2 | import { Header } from './Header'; 3 | import './page.css'; 4 | 5 | export const Page = () => { 6 | const [user, setUser] = createSignal(); 7 | 8 | return ( 9 |
10 |
setUser({ name: 'Jane Doe' })} 13 | onLogout={() => setUser(undefined)} 14 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 15 | /> 16 | 17 |
18 |

Pages in Storybook

19 |

20 | We recommend building UIs with a{' '} 21 | 26 | component-driven 27 | {' '} 28 | process starting with atomic components and ending with pages. 29 |

30 |

31 | Render pages with mock data. This makes it easy to build and review 32 | page states without needing to navigate to them in your app. Here are 33 | some handy patterns for managing page data in Storybook: 34 |

35 |
    36 |
  • 37 | Use a higher-level connected component. Storybook helps you compose 38 | such data from the "args" of child component stories 39 |
  • 40 |
  • 41 | Assemble data in the page component from your services. You can mock 42 | these services out using Storybook. 43 |
  • 44 |
45 |

46 | Get a guided tutorial on component-driven development at{' '} 47 | 52 | Storybook tutorials 53 | 54 | . Read more in the{' '} 55 | 60 | docs 61 | 62 | . 63 |

64 |
65 | Tip Adjust the width of the canvas with the{' '} 66 | 72 | 73 | 78 | 79 | 80 | Viewports addon in the toolbar 81 |
82 |
83 |
84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /packages/renderers/solid/template/cli/ts/Page.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createSignal } from 'solid-js'; 2 | import { Header } from './Header'; 3 | import './page.css'; 4 | 5 | type User = { 6 | name: string; 7 | }; 8 | 9 | export const Page: Component = () => { 10 | const [user, setUser] = createSignal(); 11 | 12 | return ( 13 |
14 |
setUser({ name: 'Jane Doe' })} 17 | onLogout={() => setUser(undefined)} 18 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 19 | /> 20 | 21 |
22 |

Pages in Storybook

23 |

24 | We recommend building UIs with a{' '} 25 | 30 | component-driven 31 | {' '} 32 | process starting with atomic components and ending with pages. 33 |

34 |

35 | Render pages with mock data. This makes it easy to build and review 36 | page states without needing to navigate to them in your app. Here are 37 | some handy patterns for managing page data in Storybook: 38 |

39 |
    40 |
  • 41 | Use a higher-level connected component. Storybook helps you compose 42 | such data from the "args" of child component stories 43 |
  • 44 |
  • 45 | Assemble data in the page component from your services. You can mock 46 | these services out using Storybook. 47 |
  • 48 |
49 |

50 | Get a guided tutorial on component-driven development at{' '} 51 | 56 | Storybook tutorials 57 | 58 | . Read more in the{' '} 59 | 64 | docs 65 | 66 | . 67 |

68 |
69 | Tip Adjust the width of the canvas with the{' '} 70 | 76 | 77 | 82 | 83 | 84 | Viewports addon in the toolbar 85 |
86 |
87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /example/src/components/StorybookLogo/storybook-logo.svg: -------------------------------------------------------------------------------- 1 | icon-storybook-default -------------------------------------------------------------------------------- /packages/renderers/solid/src/public-types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { 3 | AnnotatedStoryFn, 4 | Args, 5 | ArgsFromMeta, 6 | ArgsStoryFn, 7 | ComponentAnnotations, 8 | DecoratorFunction, 9 | LoaderFunction, 10 | ProjectAnnotations, 11 | StoryAnnotations, 12 | StoryContext as GenericStoryContext, 13 | StrictArgs, 14 | } from '@storybook/types'; 15 | import type { ComponentProps, Component as ComponentType } from 'solid-js'; 16 | import type { SetOptional, Simplify } from 'type-fest'; 17 | import type { SolidRenderer } from './types'; 18 | 19 | export type { Args, ArgTypes, Parameters, StrictArgs } from '@storybook/types'; 20 | export type { SolidRenderer }; 21 | 22 | /** 23 | * Metadata to configure the stories for a component. 24 | * 25 | * @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export) 26 | */ 27 | export type Meta = 28 | TCmpOrArgs extends ComponentType 29 | ? ComponentAnnotations> 30 | : ComponentAnnotations; 31 | 32 | /** 33 | * Story function that represents a CSFv2 component example. 34 | * 35 | * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) 36 | */ 37 | export type StoryFn = 38 | TCmpOrArgs extends ComponentType 39 | ? AnnotatedStoryFn> 40 | : AnnotatedStoryFn; 41 | 42 | /** 43 | * Story function that represents a CSFv3 component example. 44 | * 45 | * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) 46 | */ 47 | export type StoryObj = TMetaOrCmpOrArgs extends { 48 | render?: ArgsStoryFn; 49 | component?: infer Component; 50 | args?: infer DefaultArgs; 51 | } 52 | ? Simplify< 53 | (Component extends ComponentType 54 | ? ComponentProps 55 | : unknown) & 56 | ArgsFromMeta 57 | > extends infer TArgs 58 | ? StoryAnnotations< 59 | SolidRenderer, 60 | TArgs, 61 | SetOptional< 62 | TArgs, 63 | keyof TArgs & keyof (DefaultArgs & ActionArgs) 64 | > 65 | > 66 | : never 67 | : TMetaOrCmpOrArgs extends ComponentType 68 | ? StoryAnnotations> 69 | : StoryAnnotations; 70 | 71 | type ActionArgs = { 72 | // This can be read as: filter TArgs on functions where we can assign a void function to that function. 73 | // The docs addon argsEnhancers can only safely provide a default value for void functions. 74 | // Other kind of required functions should be provided by the user. 75 | [P in keyof TArgs as TArgs[P] extends (...args: any[]) => any 76 | ? ((...args: any[]) => void) extends TArgs[P] 77 | ? P 78 | : never 79 | : never]: TArgs[P]; 80 | }; 81 | 82 | export type Decorator = DecoratorFunction< 83 | SolidRenderer, 84 | TArgs 85 | >; 86 | export type Loader = LoaderFunction; 87 | export type StoryContext = GenericStoryContext< 88 | SolidRenderer, 89 | TArgs 90 | >; 91 | export type Preview = ProjectAnnotations; 92 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/renderToCanvas.tsx: -------------------------------------------------------------------------------- 1 | import type { Component } from 'solid-js'; 2 | import { ErrorBoundary, onMount } from 'solid-js'; 3 | import { createStore } from 'solid-js/store'; 4 | import { render as solidRender } from 'solid-js/web'; 5 | import { Semaphore } from 'async-mutex'; 6 | import type { RenderContext } from '@storybook/types'; 7 | import type { ComponentsData, SolidRenderer, StoryContext } from './types'; 8 | import { Decorator } from './public-types'; 9 | 10 | /** 11 | * SolidJS store for handling fine grained updates 12 | * of the components data as f.e. story args. 13 | */ 14 | const [store, setStore] = createStore({} as ComponentsData); 15 | 16 | /** 17 | * A decorator that ensures changing args updates the story. 18 | */ 19 | export const solidReactivityDecorator: Decorator = (Story, context) => { 20 | const storyId = context.canvasElement.id; 21 | context.args = store[storyId].args; 22 | return ; 23 | }; 24 | 25 | /** 26 | * Resets an specific story store. 27 | */ 28 | const cleanStoryStore = (storeId: string) => { 29 | setStore({ [storeId]: { args: {}, rendered: false, disposeFn: () => {} } }); 30 | }; 31 | 32 | /** 33 | * Disposes an specific story. 34 | */ 35 | const disposeStory = (storeId: string) => { 36 | store[storeId]?.disposeFn?.(); 37 | }; 38 | 39 | /** 40 | * This function resets the canvas and reactive store for an specific story. 41 | */ 42 | const remountStory = (storyId: string) => { 43 | disposeStory(storyId); 44 | cleanStoryStore(storyId); 45 | }; 46 | 47 | /** 48 | * Checks if the story store exists 49 | */ 50 | const storyIsRendered = (storyId: string) => Boolean(store[storyId]?.rendered); 51 | 52 | /** 53 | * Renders solid App into DOM. 54 | */ 55 | const renderSolidApp = ( 56 | storyId: string, 57 | renderContext: RenderContext, 58 | canvasElement: SolidRenderer['canvasElement'], 59 | ) => { 60 | const { storyContext, storyFn, showMain, showException } = renderContext; 61 | 62 | setStore(storyId, 'rendered', true); 63 | 64 | const App: Component = () => { 65 | const Story = storyFn as Component>; 66 | 67 | onMount(() => { 68 | showMain(); 69 | }); 70 | 71 | return ( 72 | { 74 | showException(err); 75 | return err; 76 | }} 77 | > 78 | 79 | 80 | ); 81 | }; 82 | 83 | return solidRender(() => , canvasElement as HTMLElement); 84 | }; 85 | 86 | const semaphore = new Semaphore(1); 87 | 88 | /** 89 | * Main renderer function for initializing the SolidJS app with the story content. 90 | * 91 | * How this works is a bit different from the React renderer. 92 | * In React, components run again on rerender so the React renderer just recalls the component, 93 | * but Solid has fine-grained reactivity so components run once, 94 | * and when dependencies are updated, effects/tracking scopes run again. 95 | * 96 | * So, we can store args in a store and just update the store when this function is called. 97 | */ 98 | export async function renderToCanvas( 99 | renderContext: RenderContext, 100 | canvasElement: SolidRenderer['canvasElement'], 101 | ) { 102 | const { storyContext, forceRemount } = renderContext; 103 | const storyId = storyContext.canvasElement.id; 104 | 105 | // Story is remounted given the conditions. 106 | if (forceRemount) { 107 | remountStory(storyId); 108 | } 109 | 110 | // Story store data is updated 111 | setStore(storyId, 'args', storyContext.args); 112 | 113 | // Story is rendered and store data is created 114 | if (storyIsRendered(storyId) === false) { 115 | await semaphore.runExclusive(async () => { 116 | const disposeFn = renderSolidApp(storyId, renderContext, canvasElement); 117 | setStore(storyId, (prev) => ({ ...prev, disposeFn })); 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/docs/sourceDecorator.test.tsx: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, test, vi } from 'vitest'; 2 | import { generateSolidSource } from './sourceDecorator'; 3 | 4 | test('plain component', () => { 5 | const newSrc1 = generateSolidSource('Component', '{ }'); 6 | 7 | expect(newSrc1).toMatchInlineSnapshot(`""`); 8 | 9 | const newSrc2 = generateSolidSource('Component', '{ args: { } }'); 10 | 11 | expect(newSrc2).toMatchInlineSnapshot(`""`); 12 | }); 13 | 14 | test('component with props', () => { 15 | const newSrc = generateSolidSource( 16 | 'Component', 17 | '{ args: { foo: 32, bar: "Hello" } }', 18 | ); 19 | 20 | expect(newSrc).toMatchInlineSnapshot( 21 | `""`, 22 | ); 23 | }); 24 | 25 | test('component with children', () => { 26 | const newSrc = generateSolidSource( 27 | 'Component', 28 | '{ args: { children: "Hello" } }', 29 | ); 30 | 31 | expect(newSrc).toMatchInlineSnapshot(`"Hello"`); 32 | }); 33 | 34 | test('component with true prop', () => { 35 | const newSrc = generateSolidSource('Component', '{ args: { foo: true } }'); 36 | 37 | expect(newSrc).toMatchInlineSnapshot(`""`); 38 | }); 39 | 40 | test('component with props and children', () => { 41 | const newSrc = generateSolidSource( 42 | 'Component', 43 | '{ args: { foo: 32, children: "Hello" } }', 44 | ); 45 | 46 | expect(newSrc).toMatchInlineSnapshot( 47 | `"Hello"`, 48 | ); 49 | }); 50 | 51 | test('component with method prop', () => { 52 | const newSrc = generateSolidSource( 53 | 'Component', 54 | '{ args: { search() { return 32; } } }', 55 | ); 56 | 57 | expect(newSrc).toMatchInlineSnapshot(` 58 | " { 59 | return 32; 60 | }} />" 61 | `); 62 | }); 63 | 64 | test('component with typescript', () => { 65 | const newSrc = generateSolidSource( 66 | 'Component', 67 | '{ args: { double: (x: number) => { return x * 2; } } }', 68 | ); 69 | 70 | expect(newSrc).toMatchInlineSnapshot(` 71 | " { 72 | return x * 2; 73 | }} />" 74 | `); 75 | }); 76 | 77 | test('component with expression children', () => { 78 | const newSrc = generateSolidSource( 79 | 'Component', 80 | '{ args: { children: { do: () => { return 32; } } } }', 81 | ); 82 | 83 | expect(newSrc).toMatchInlineSnapshot(` 84 | "{{ 85 | do: () => { 86 | return 32; 87 | } 88 | }}" 89 | `); 90 | }); 91 | 92 | test('component with render function', () => { 93 | const newSrc = generateSolidSource( 94 | 'Component', 95 | '{ render: () => Hello }', 96 | ); 97 | 98 | expect(newSrc).toMatchInlineSnapshot( 99 | `"Hello"`, 100 | ); 101 | }); 102 | 103 | test('component with render function and args', () => { 104 | const newSrc = generateSolidSource( 105 | 'Component', 106 | '{ args: { foo: 32 }, render: (args) => Hello }', 107 | ); 108 | 109 | expect(newSrc).toMatchInlineSnapshot(` 110 | "const args = { 111 | foo: 32 112 | }; 113 | 114 | Hello" 115 | `); 116 | }); 117 | 118 | test('component with render function and missing args', () => { 119 | const newSrc = generateSolidSource( 120 | 'Component', 121 | '{ render: (args) => Hello }', 122 | ); 123 | 124 | expect(newSrc).toMatchInlineSnapshot(` 125 | "const args = {}; 126 | 127 | Hello" 128 | `); 129 | }); 130 | 131 | test('component with render function and args and ctx', () => { 132 | const newSrc = generateSolidSource( 133 | 'Component', 134 | '{ args: { foo: 32 }, render: (args, ctx) => Hello }', 135 | ); 136 | 137 | expect(newSrc).toMatchInlineSnapshot(` 138 | "const args = { 139 | foo: 32 140 | }; 141 | 142 | var ctx; 143 | 144 | Hello" 145 | `); 146 | }); 147 | 148 | test('component missing story config', () => { 149 | const newSrc = () => generateSolidSource('Component', '5 + 4'); 150 | 151 | expect(newSrc).toThrow('Expected `ObjectExpression` type'); 152 | }); 153 | 154 | test('component has invalid args', () => { 155 | const newSrc = () => generateSolidSource('Component', '{ args: 5 }'); 156 | 157 | expect(newSrc).toThrow('Expected `ObjectExpression` type'); 158 | }); 159 | 160 | describe('catch warnings for skipped props', () => { 161 | const consoleMock = vi.spyOn(console, 'warn').mockImplementation(() => {}); 162 | 163 | afterAll(() => { 164 | consoleMock.mockReset(); 165 | }); 166 | 167 | test('component prop has computed name', () => { 168 | const newSrc = generateSolidSource( 169 | 'Component', 170 | '{ args: { ["foo"]: 32 } }', 171 | ); 172 | 173 | expect(newSrc).toMatchInlineSnapshot(`""`); 174 | expect(consoleMock).toHaveBeenCalledWith( 175 | 'Encountered computed key, skipping...', 176 | ); 177 | }); 178 | 179 | test('component method has computed name', () => { 180 | const newSrc = generateSolidSource( 181 | 'Component', 182 | '{ args: { ["foo"]() { return 32; } } }', 183 | ); 184 | 185 | expect(newSrc).toMatchInlineSnapshot(`""`); 186 | expect(consoleMock).toHaveBeenCalledWith( 187 | 'Encountered computed key, skipping...', 188 | ); 189 | }); 190 | 191 | test('component method is a getter or setter', () => { 192 | const newSrc = generateSolidSource( 193 | 'Component', 194 | '{ args: { get foo() { return 32; } } }', 195 | ); 196 | 197 | expect(newSrc).toMatchInlineSnapshot(`""`); 198 | expect(consoleMock).toHaveBeenCalledWith( 199 | 'Encountered getter or setter, skipping...', 200 | ); 201 | }); 202 | 203 | test('component prop is a spread element', () => { 204 | const newSrc = generateSolidSource('Component', '{ args: { ...foo } }'); 205 | 206 | expect(newSrc).toMatchInlineSnapshot(`""`); 207 | expect(consoleMock).toHaveBeenCalledWith( 208 | 'Encountered spread element, skipping...', 209 | ); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /scripts/prepare/build.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises'; 2 | import { dirname, join, parse, posix, relative, resolve, sep } from 'node:path'; 3 | 4 | import type { Metafile } from 'esbuild'; 5 | import aliasPlugin from 'esbuild-plugin-alias'; 6 | import { solidPlugin } from 'esbuild-plugin-solid'; 7 | import * as fs from 'fs-extra'; 8 | import { fdir } from 'fdir'; 9 | import slash from 'slash'; 10 | import { dedent } from 'ts-dedent'; 11 | import { build, Format, type Options } from 'tsup'; 12 | import type { PackageJson } from 'type-fest'; 13 | 14 | import { exec } from '../utils/exec'; 15 | import * as esbuild from 'esbuild'; 16 | 17 | /* TYPES */ 18 | 19 | type Formats = 'esm' | 'cjs'; 20 | type BundlerConfig = { 21 | entries: string[]; 22 | externals: string[]; 23 | noExternal: string[]; 24 | platform: Options['platform']; 25 | pre: string; 26 | post: string; 27 | formats: Formats[]; 28 | }; 29 | type PackageJsonWithBundlerConfig = PackageJson & { 30 | bundler: BundlerConfig; 31 | }; 32 | type DtsConfigSection = Pick; 33 | 34 | /* MAIN */ 35 | 36 | const OUT_DIR = join(process.cwd(), 'dist'); 37 | 38 | const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { 39 | const { 40 | name, 41 | dependencies, 42 | peerDependencies, 43 | bundler: { 44 | entries = [], 45 | externals: extraExternals = [], 46 | noExternal: extraNoExternal = [], 47 | platform, 48 | pre, 49 | post, 50 | formats = ['esm', 'cjs'], 51 | }, 52 | } = (await fs.readJson( 53 | join(cwd, 'package.json'), 54 | )) as PackageJsonWithBundlerConfig; 55 | 56 | if (pre) { 57 | await exec(`jiti ${pre}`, { cwd }); 58 | } 59 | 60 | if (!name) throw 'No package name found!'; 61 | 62 | const metafilesDir = join( 63 | __dirname, 64 | '..', 65 | '..', 66 | 'bench', 67 | 'esbuild-metafiles', 68 | name, 69 | ); 70 | 71 | const reset = hasFlag(flags, 'reset'); 72 | const watch = hasFlag(flags, 'watch'); 73 | const optimized = hasFlag(flags, 'optimized'); 74 | if (reset) { 75 | await fs.emptyDir(OUT_DIR); 76 | await fs.emptyDir(metafilesDir); 77 | } 78 | 79 | const externals = [ 80 | name, 81 | ...extraExternals, 82 | ...Object.keys(dependencies || {}), 83 | ...Object.keys(peerDependencies || {}), 84 | ]; 85 | 86 | const allEntries = entries.map((e: string) => slash(join(cwd, e))); 87 | 88 | const { dtsBuild, dtsConfig, tsConfigExists } = await getDTSConfigs({ 89 | formats, 90 | entries, 91 | optimized, 92 | }); 93 | 94 | /* preset files are always CJS only. 95 | * Generating an ESM file for them anyway is problematic because they often have a reference to `require`. 96 | * TSUP generated code will then have a `require` polyfill/guard in the ESM files, which causes issues for webpack. 97 | */ 98 | const nonPresetEntries = allEntries.filter( 99 | (f) => !parse(f).name.includes('preset'), 100 | ); 101 | const presetEntries = allEntries.filter((f) => 102 | parse(f).name.includes('preset'), 103 | ); 104 | 105 | const noExternal = [...extraNoExternal]; 106 | 107 | const outExtension = ({ format }: { format: Format }) => { 108 | if (format == 'esm') return { js: '.mjs' }; 109 | return { js: '.js' }; 110 | }; 111 | 112 | await build({ 113 | noExternal, 114 | silent: true, 115 | treeshake: true, 116 | entry: nonPresetEntries, 117 | shims: false, 118 | watch, 119 | outDir: OUT_DIR, 120 | sourcemap: false, 121 | metafile: true, 122 | format: formats, 123 | target: 124 | platform === 'node' ? ['node18'] : ['chrome100', 'safari15', 'firefox91'], 125 | clean: false, 126 | ...(dtsBuild === 'esm' ? dtsConfig : {}), 127 | platform: platform || 'browser', 128 | plugins: 129 | platform === 'node' 130 | ? [solidPlugin()] 131 | : [ 132 | solidPlugin(), 133 | aliasPlugin({ 134 | process: resolve('../node_modules/process/browser.js'), 135 | util: resolve('../node_modules/util/util.js'), 136 | }), 137 | ], 138 | external: externals, 139 | outExtension, 140 | 141 | esbuildOptions: (c) => { 142 | c.conditions = ['module']; 143 | c.platform = platform || 'browser'; 144 | Object.assign(c, getESBuildOptions(optimized)); 145 | }, 146 | }); 147 | 148 | if (formats.includes('cjs') && presetEntries.length > 0) { 149 | await build({ 150 | noExternal, 151 | silent: true, 152 | entry: presetEntries, 153 | watch, 154 | outDir: OUT_DIR, 155 | sourcemap: false, 156 | metafile: true, 157 | format: ['cjs'], 158 | target: 'node18', 159 | ...(dtsBuild === 'cjs' ? dtsConfig : {}), 160 | platform: 'node', 161 | clean: false, 162 | plugins: [solidPlugin()], 163 | external: externals, 164 | outExtension, 165 | 166 | esbuildOptions: (c) => { 167 | c.platform = 'node'; 168 | Object.assign(c, getESBuildOptions(optimized)); 169 | }, 170 | }); 171 | } 172 | 173 | if (tsConfigExists && !optimized) { 174 | await Promise.all(entries.map(generateDTSMapperFile)); 175 | } 176 | 177 | if (!watch) { 178 | await saveMetafiles({ metafilesDir, formats }); 179 | } 180 | 181 | const dtsFiles = await new fdir() 182 | .withFullPaths() 183 | .glob('/**/*.d.ts') 184 | .crawl(OUT_DIR) 185 | .withPromise(); 186 | await Promise.all( 187 | dtsFiles.map(async (file) => { 188 | const content = await fs.readFile(file, 'utf-8'); 189 | await fs.writeFile( 190 | file, 191 | content.replace( 192 | /from 'core\/dist\/(.*)'/g, 193 | `from 'storybook/internal/$1'`, 194 | ), 195 | ); 196 | }), 197 | ); 198 | 199 | if (post) { 200 | await exec(`jiti ${post}`, { cwd }, { debug: true }); 201 | } 202 | 203 | if (process.env.CI !== 'true') { 204 | console.log('done'); 205 | } 206 | }; 207 | 208 | /* UTILS */ 209 | 210 | async function getDTSConfigs({ 211 | formats, 212 | entries, 213 | optimized, 214 | }: { 215 | formats: Formats[]; 216 | entries: string[]; 217 | optimized: boolean; 218 | }) { 219 | const tsConfigPath = join(cwd, 'tsconfig.json'); 220 | const tsConfigExists = await fs.pathExists(tsConfigPath); 221 | 222 | const dtsBuild = 223 | optimized && formats[0] && tsConfigExists ? formats[0] : undefined; 224 | 225 | const dtsConfig: DtsConfigSection = { 226 | tsconfig: tsConfigPath, 227 | dts: { 228 | entry: entries, 229 | resolve: true, 230 | }, 231 | }; 232 | 233 | return { dtsBuild, dtsConfig, tsConfigExists }; 234 | } 235 | 236 | function getESBuildOptions(optimized: boolean) { 237 | return { 238 | logLevel: 'error', 239 | legalComments: 'none', 240 | minifyWhitespace: optimized, 241 | minifyIdentifiers: false, 242 | minifySyntax: optimized, 243 | }; 244 | } 245 | 246 | async function generateDTSMapperFile(file: string) { 247 | const { name: entryName, dir } = parse(file); 248 | 249 | const pathName = join( 250 | process.cwd(), 251 | dir.replace('./src', 'dist'), 252 | `${entryName}.d.ts`, 253 | ); 254 | const srcName = join(process.cwd(), file); 255 | const rel = relative(dirname(pathName), dirname(srcName)) 256 | .split(sep) 257 | .join(posix.sep); 258 | 259 | await fs.ensureFile(pathName); 260 | await fs.writeFile( 261 | pathName, 262 | dedent` 263 | // dev-mode 264 | export * from '${rel}/${entryName}'; 265 | `, 266 | { encoding: 'utf-8' }, 267 | ); 268 | } 269 | 270 | async function saveMetafiles({ 271 | metafilesDir, 272 | formats, 273 | }: { 274 | metafilesDir: string; 275 | formats: Formats[]; 276 | }) { 277 | await fs.ensureDir(metafilesDir); 278 | const metafile: Metafile = { 279 | inputs: {}, 280 | outputs: {}, 281 | }; 282 | 283 | await Promise.all( 284 | formats.map(async (format) => { 285 | const fromFilename = `metafile-${format}.json`; 286 | const currentMetafile = await fs.readJson(join(OUT_DIR, fromFilename)); 287 | metafile.inputs = { ...metafile.inputs, ...currentMetafile.inputs }; 288 | metafile.outputs = { ...metafile.outputs, ...currentMetafile.outputs }; 289 | 290 | await fs.rm(join(OUT_DIR, fromFilename)); 291 | }), 292 | ); 293 | 294 | await writeFile( 295 | join(metafilesDir, 'metafile.json'), 296 | JSON.stringify(metafile, null, 2), 297 | ); 298 | await writeFile( 299 | join(metafilesDir, 'metafile.txt'), 300 | await esbuild.analyzeMetafile(metafile, { color: false, verbose: false }), 301 | ); 302 | } 303 | 304 | const hasFlag = (flags: string[], name: string) => 305 | !!flags.find((s) => s.startsWith(`--${name}`)); 306 | 307 | /* SELF EXECUTION */ 308 | 309 | const flags = process.argv.slice(2); 310 | const cwd = process.cwd(); 311 | 312 | run({ cwd, flags }).catch((err: unknown) => { 313 | // We can't let the stack try to print, it crashes in a way that sets the exit code to 0. 314 | // Seems to have something to do with running JSON.parse() on binary / base64 encoded sourcemaps 315 | // in @cspotcode/source-map-support 316 | if (err instanceof Error) { 317 | console.error(err.stack); 318 | } 319 | process.exit(1); 320 | }); 321 | -------------------------------------------------------------------------------- /packages/renderers/solid/src/docs/sourceDecorator.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | // @babel/standalone does not export types, 4 | // so this file is a mess of anys. 5 | 6 | import type { StoryContext, PartialStoryFn } from '@storybook/types'; 7 | import { SolidRenderer } from '../types'; 8 | 9 | import { SNIPPET_RENDERED, SourceType } from '@storybook/docs-tools'; 10 | import { addons, useEffect } from '@storybook/preview-api'; 11 | 12 | // @ts-expect-error Types are not up to date 13 | import * as Babel from '@babel/standalone'; 14 | const parser = Babel.packages.parser; 15 | const generate = Babel.packages.generator.default; 16 | const t = Babel.packages.types; 17 | 18 | function skipSourceRender(context: StoryContext): boolean { 19 | const sourceParams = context?.parameters.docs?.source; 20 | const isArgsStory = context?.parameters.__isArgsStory; 21 | 22 | // always render if the user forces it 23 | if (sourceParams?.type === SourceType.DYNAMIC) { 24 | return false; 25 | } 26 | 27 | // never render if the user is forcing the block to render code, or 28 | // if the user provides code, or if it's not an args story. 29 | return ( 30 | !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE 31 | ); 32 | } 33 | 34 | /** 35 | * Generate JSX source code from stories. 36 | */ 37 | export const sourceDecorator = ( 38 | storyFn: PartialStoryFn, 39 | ctx: StoryContext, 40 | ) => { 41 | // Strategy: Since SolidJS doesn't have a VDOM, 42 | // it isn't possible to get information directly about inner components. 43 | // Instead, there needs to be an altered render function 44 | // that records information about component properties, 45 | // or source code extraction from files. 46 | // This decorator uses the latter technique. 47 | // By using the source code string generated by CSF-tools, 48 | // we can then parse the properties of the `args` object, 49 | // and return the source slices. 50 | 51 | // Note: this also means we are limited in how we can 52 | // get the component name. 53 | // Since Storybook doesn't do source code extraction for 54 | // story metas (yet), we can use the title for now. 55 | const channel = addons.getChannel(); 56 | const story = storyFn(); 57 | const skip = skipSourceRender(ctx); 58 | 59 | let source: string | null = null; 60 | 61 | useEffect(() => { 62 | if (!skip && source) { 63 | const { id, unmappedArgs } = ctx; 64 | channel.emit(SNIPPET_RENDERED, { id, args: unmappedArgs, source }); 65 | } 66 | }); 67 | 68 | if (skip) return story; 69 | 70 | const docs = ctx?.parameters?.docs; 71 | const src = docs?.source?.originalSource; 72 | const name = ctx.title.split('/').at(-1)!; 73 | 74 | try { 75 | source = generateSolidSource(name, src); 76 | } catch (e) { 77 | console.error(e); 78 | } 79 | 80 | return story; 81 | }; 82 | 83 | /** 84 | * Generate Solid JSX from story source. 85 | */ 86 | export function generateSolidSource(name: string, src: string): string { 87 | const ast = parser.parseExpression(src, { plugins: ['jsx', 'typescript'] }); 88 | const { attributes, children, original } = parseArgs(ast); 89 | const render = parseRender(ast); 90 | 91 | // If there is a render function, display it to the best of our ability. 92 | if (render) { 93 | const { body, params } = render; 94 | let newSrc = ''; 95 | 96 | // Add arguments declaration. 97 | if (params[0]) { 98 | const args = original ?? { 99 | type: 'ObjectExpression', 100 | properties: [], 101 | }; 102 | 103 | const argsStatement = { 104 | type: 'VariableDeclaration', 105 | kind: 'const', 106 | declarations: [ 107 | { 108 | type: 'VariableDeclarator', 109 | id: { 110 | type: 'Identifier', 111 | name: params[0], 112 | }, 113 | init: args, 114 | }, 115 | ], 116 | }; 117 | 118 | newSrc += generate(argsStatement, { compact: false }).code + '\n\n'; 119 | } 120 | 121 | // Add context declaration. 122 | if (params[1]) { 123 | const ctxStatement = { 124 | type: 'VariableDeclaration', 125 | kind: 'var', 126 | declarations: [ 127 | { 128 | type: 'VariableDeclarator', 129 | id: { 130 | type: 'Identifier', 131 | name: params[1], 132 | }, 133 | }, 134 | ], 135 | }; 136 | 137 | newSrc += generate(ctxStatement, { compact: false }).code + '\n\n'; 138 | } 139 | 140 | newSrc += generate(body, { compact: false }).code; 141 | 142 | return newSrc; 143 | } 144 | 145 | // Otherwise, render a component with the arguments. 146 | 147 | const selfClosing = children == null || children.length == 0; 148 | 149 | const component = { 150 | type: 'JSXElement', 151 | openingElement: { 152 | type: 'JSXOpeningElement', 153 | name: { 154 | type: 'JSXIdentifier', 155 | name, 156 | }, 157 | attributes: attributes, 158 | selfClosing, 159 | }, 160 | children: children ?? [], 161 | closingElement: selfClosing 162 | ? undefined 163 | : { 164 | type: 'JSXClosingElement', 165 | name: { 166 | type: 'JSXIdentifier', 167 | name, 168 | }, 169 | }, 170 | }; 171 | 172 | return generate(component, { compact: false }).code; 173 | } 174 | 175 | /** 176 | * Convert any AST node to a JSX child node. 177 | */ 178 | function toJSXChild(node: any): object { 179 | if ( 180 | t.isJSXElement(node) || 181 | t.isJSXText(node) || 182 | t.isJSXExpressionContainer(node) || 183 | t.isJSXSpreadChild(node) || 184 | t.isJSXFragment(node) 185 | ) { 186 | return node; 187 | } 188 | 189 | if (t.isStringLiteral(node)) { 190 | return { 191 | type: 'JSXText', 192 | value: node.value, 193 | }; 194 | } 195 | 196 | if (t.isExpression(node)) { 197 | return { 198 | type: 'JSXExpressionContainer', 199 | expression: node, 200 | }; 201 | } 202 | 203 | return { 204 | type: 'JSXExpressionContainer', 205 | expression: t.jsxEmptyExpression(), 206 | }; 207 | } 208 | /** Story render function. */ 209 | interface SolidRender { 210 | body: object; 211 | params: string[]; 212 | } 213 | 214 | function parseRender(ast: any): SolidRender | null { 215 | if (ast.type != 'ObjectExpression') throw 'Expected `ObjectExpression` type'; 216 | // Find render property. 217 | const renderProp = ast.properties.find((v: any) => { 218 | if (v.type != 'ObjectProperty') return false; 219 | if (v.key.type != 'Identifier') return false; 220 | return v.key.name == 'render'; 221 | }) as any | undefined; 222 | if (!renderProp) return null; 223 | 224 | const renderFn = renderProp.value; 225 | if ( 226 | renderFn.type != 'ArrowFunctionExpression' && 227 | renderFn.type != 'FunctionExpression' 228 | ) { 229 | console.warn('`render` property is not a function, skipping...'); 230 | return null; 231 | } 232 | 233 | return { 234 | body: renderFn.body, 235 | params: renderFn.params.map((x: any) => x.name), 236 | }; 237 | } 238 | 239 | /** Story arguments. */ 240 | interface SolidArgs { 241 | attributes: object[]; 242 | children: object[] | null; 243 | original: object | null; 244 | } 245 | 246 | /** 247 | * Parses component arguments from source expression. 248 | * 249 | * The source code will be in the form of a `Story` object. 250 | */ 251 | function parseArgs(ast: any): SolidArgs { 252 | if (ast.type != 'ObjectExpression') throw 'Expected `ObjectExpression` type'; 253 | // Find args property. 254 | const argsProp = ast.properties.find((v: any) => { 255 | if (v.type != 'ObjectProperty') return false; 256 | if (v.key.type != 'Identifier') return false; 257 | return v.key.name == 'args'; 258 | }) as any | undefined; 259 | // No args, so there aren't any properties or children. 260 | if (!argsProp) 261 | return { 262 | attributes: [], 263 | children: null, 264 | original: null, 265 | }; 266 | // Get arguments. 267 | const original = argsProp.value; 268 | if (original.type != 'ObjectExpression') 269 | throw 'Expected `ObjectExpression` type'; 270 | 271 | // Construct props object, where values are source code slices. 272 | const attributes: object[] = []; 273 | let children: object[] | null = null; 274 | for (const el of original.properties) { 275 | let attr: object | null = null; 276 | 277 | switch (el.type) { 278 | case 'ObjectProperty': 279 | if (el.key.type != 'Identifier') { 280 | console.warn('Encountered computed key, skipping...'); 281 | continue; 282 | } 283 | if (el.key.name == 'children') { 284 | children = [toJSXChild(el.value)]; 285 | continue; 286 | } 287 | 288 | attr = parseProperty(el); 289 | break; 290 | case 'ObjectMethod': 291 | attr = parseMethod(el); 292 | break; 293 | case 'SpreadElement': 294 | // Spread elements use external values, should not be used. 295 | console.warn('Encountered spread element, skipping...'); 296 | continue; 297 | } 298 | 299 | if (attr) { 300 | attributes.push(attr); 301 | } 302 | } 303 | 304 | return { attributes, children, original }; 305 | } 306 | 307 | /** 308 | * Parse an object property. 309 | * 310 | * JSX flag attributes are mapped from boolean literals. 311 | */ 312 | function parseProperty(el: any): object | null { 313 | let value: any = { 314 | type: 'JSXExpressionContainer', 315 | expression: el.value, 316 | }; 317 | 318 | if (el.value.type == 'BooleanLiteral' && el.value.value == true) { 319 | value = undefined; 320 | } 321 | 322 | return { 323 | type: 'JSXAttribute', 324 | name: { 325 | type: 'JSXIdentifier', 326 | name: el.key.name, 327 | }, 328 | value, 329 | }; 330 | } 331 | 332 | /** 333 | * Parse an object method. 334 | * 335 | * Note that object methods cannot be generators. 336 | * This means that methods can be mapped straight to arrow functions. 337 | */ 338 | function parseMethod(el: any): object | null { 339 | if (el.kind != 'method') { 340 | console.warn('Encountered getter or setter, skipping...'); 341 | return null; 342 | } 343 | 344 | if (el.key.type != 'Identifier') { 345 | console.warn('Encountered computed key, skipping...'); 346 | return null; 347 | } 348 | 349 | const { params, body, async, returnType, typeParameters } = el; 350 | 351 | return { 352 | type: 'JSXAttribute', 353 | name: { 354 | type: 'JSXIdentifier', 355 | name: el.key.name, 356 | }, 357 | value: { 358 | type: 'JSXExpressionContainer', 359 | expression: { 360 | type: 'ArrowFunctionExpression', 361 | params, 362 | body, 363 | async, 364 | expression: false, 365 | returnType, 366 | typeParameters, 367 | }, 368 | }, 369 | }; 370 | } 371 | --------------------------------------------------------------------------------