├── .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 |
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 |
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 | You need to enable JavaScript to run this app.
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 |
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 |
30 | {local.label}
31 |
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 |
53 | {local.label}
54 |
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 |
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 |
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 |
--------------------------------------------------------------------------------