├── .codesandbox └── tasks.json ├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── local-preset.js ├── main.ts ├── preview-head.html └── preview.tsx ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── manager.js ├── package.json ├── pnpm-lock.yaml ├── preset.js ├── preview.js ├── scripts ├── eject-typescript.js ├── prepublish-checks.js └── welcome.js ├── src ├── Tool.tsx ├── constants.ts ├── index.ts ├── manager.tsx ├── preset.ts ├── stories │ ├── Button.stories.ts │ ├── Text.mdx │ └── Text.stories.tsx └── utils.ts ├── tsconfig.json ├── tsup.config.ts └── vite.config.ts /.codesandbox/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // These tasks will run in order when initializing your CodeSandbox project. 3 | "setupTasks": [ 4 | { 5 | "name": "Install Dependencies", 6 | "command": "pnpm install" 7 | } 8 | ], 9 | 10 | // These tasks can be run from CodeSandbox. Running one will open a log in the app. 11 | "tasks": { 12 | "start": { 13 | "name": "start", 14 | "command": "pnpm run start" 15 | }, 16 | "postinstall": { 17 | "name": "postinstall", 18 | "command": "pnpm run postinstall" 19 | }, 20 | "clean": { 21 | "name": "clean", 22 | "command": "pnpm run clean" 23 | }, 24 | "prebuild": { 25 | "name": "prebuild", 26 | "command": "pnpm run prebuild" 27 | }, 28 | "build": { 29 | "name": "build", 30 | "command": "pnpm run build", 31 | "runAtStart": true 32 | }, 33 | "build:watch": { 34 | "name": "build:watch", 35 | "command": "pnpm run build:watch" 36 | }, 37 | "test": { 38 | "name": "test", 39 | "command": "pnpm run test" 40 | }, 41 | "prerelease": { 42 | "name": "prerelease", 43 | "command": "pnpm run prerelease" 44 | }, 45 | "release": { 46 | "name": "release", 47 | "command": "pnpm run release" 48 | }, 49 | "eject-ts": { 50 | "name": "eject-ts", 51 | "command": "pnpm run eject-ts" 52 | }, 53 | "storybook": { 54 | "name": "storybook", 55 | "command": "pnpm run storybook", 56 | "runAtStart": true, 57 | "preview": { 58 | "port": 6006 59 | } 60 | }, 61 | "build-storybook": { 62 | "name": "build-storybook", 63 | "command": "pnpm run build-storybook" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "yarn install", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Prepare repository 16 | run: git fetch --unshallow --tags 17 | 18 | - name: Use Node.js 20.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.x 22 | 23 | - name: Install dependencies 24 | run: npm install --ignore-scripts 25 | 26 | - name: Authenticate with Registry 27 | run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc 28 | env: 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | 31 | - name: Pre-release 32 | run: npm run prepublish 33 | 34 | - name: Publish 35 | run: | 36 | npm publish 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | storybook-static/ 4 | build-storybook.log 5 | .DS_Store 6 | .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.storybook/local-preset.js: -------------------------------------------------------------------------------- 1 | function managerEntries(entry = []) { 2 | return [...entry, require.resolve("../dist/manager.js")]; 3 | } 4 | 5 | module.exports = { 6 | managerEntries, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | const config: StorybookConfig = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | "./local-preset.js", 9 | ], 10 | framework: { 11 | name: "@storybook/react-vite", 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: "tag", 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Preview } from "@storybook/react"; 4 | import { Theme } from "@radix-ui/themes"; 5 | 6 | const preview: Preview = { 7 | parameters: { 8 | codesandbox: { 9 | apiToken: import.meta.env.VITE_CSB_API_KEY, 10 | 11 | fallbackImport: "@radix-ui/themes", 12 | 13 | privacy: "public", 14 | 15 | dependencies: { 16 | "@radix-ui/themes": "latest", 17 | }, 18 | 19 | provider: `import { Theme } from "@radix-ui/themes"; 20 | import '@radix-ui/themes/styles.css'; 21 | 22 | export default ThemeProvider = ({ children }) => { 23 | return ( 24 | 25 | {children} 26 | 27 | ) 28 | }`, 29 | }, 30 | }, 31 | decorators: [ 32 | (Story) => ( 33 | 34 | 35 | 36 | ), 37 | ], 38 | }; 39 | 40 | export default preview; 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Addon structure 4 | 5 | The file outlines the structure of the addon's codebase: 6 | 7 | - `src/Tool.tsx`: "Open in CodeSandbox" button implementation and Storybook parameters parsers. 8 | - `src/manager.tsx`: addon configuration. 9 | - `.storybook`: this directory holds development configuration for testing the addon. 10 | - `./preview.tsx`: example of an ideal global parameteres setup. 11 | - `src/stories/Button.stories.ts`: example of an ideal story parameter setup. 12 | 13 | ## Commands 14 | 15 | In order to test the addon, you should run two commands: 16 | 17 | 1. `pnpm run build`: to build the CodeSandbox addon (re-run on every change); 18 | 2. `pnpm run storybook`: to open the Storybook instance with the new addon (reload every change); 19 | 20 | Or, just use `pnpm run start` 21 | 22 | ## Deployment 23 | 24 | The GitHub Action (`.github/workflows/release.yml`) should automatically publish new versions to NPM. However, it's **necessary** to manually update the `package.json#version` number to bump the library version. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent 20 | (50%) or more of the outstanding shares, or (iii) beneficial ownership of 21 | such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, and 28 | configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or Object form, 35 | made available under the License, as indicated by a copyright notice that is 36 | included in or attached to the work (an example is provided in the Appendix 37 | below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object form, 40 | that is based on (or derived from) the Work and for which the editorial 41 | revisions, annotations, elaborations, or other modifications represent, as a 42 | whole, an original work of authorship. For the purposes of this License, 43 | Derivative Works shall not include works that remain separable from, or 44 | merely link (or bind by name) to the interfaces of, the Work and Derivative 45 | Works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including the original 48 | version of the Work and any modifications or additions to that Work or 49 | Derivative Works thereof, that is intentionally submitted to Licensor for 50 | inclusion in the Work by the copyright owner or by an individual or Legal 51 | Entity authorized to submit on behalf of the copyright owner. For the 52 | purposes of this definition, "submitted" means any form of electronic, 53 | verbal, or written communication sent to the Licensor or its 54 | representatives, including but not limited to communication on electronic 55 | mailing lists, source code control systems, and issue tracking systems that 56 | are managed by, or on behalf of, the Licensor for the purpose of discussing 57 | and improving the Work, but excluding communication that is conspicuously 58 | marked or otherwise designated in writing by the copyright owner as "Not a 59 | Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity on 62 | behalf of whom a Contribution has been received by Licensor and subsequently 63 | incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of this 66 | License, each Contributor hereby grants to You a perpetual, worldwide, 67 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 68 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 69 | sublicense, and distribute the Work and such Derivative Works in Source or 70 | Object form. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of this 73 | License, each Contributor hereby grants to You a perpetual, worldwide, 74 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 75 | this section) patent license to make, have made, use, offer to sell, sell, 76 | import, and otherwise transfer the Work, where such license applies only to 77 | those patent claims licensable by such Contributor that are necessarily 78 | infringed by their Contribution(s) alone or by combination of their 79 | Contribution(s) with the Work to which such Contribution(s) was submitted. 80 | If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this 84 | License for that Work shall terminate as of the date such litigation is 85 | filed. 86 | 87 | 4. Redistribution. You may reproduce and distribute copies of the Work or 88 | Derivative Works thereof in any medium, with or without modifications, and 89 | in Source or Object form, provided that You meet the following conditions: 90 | 91 | (a) You must give any other recipients of the Work or Derivative Works a 92 | copy of this License; and 93 | 94 | (b) You must cause any modified files to carry prominent notices stating 95 | that You changed the files; and 96 | 97 | (c) You must retain, in the Source form of any Derivative Works that You 98 | distribute, all copyright, patent, trademark, and attribution notices from 99 | the Source form of the Work, excluding those notices that do not pertain to 100 | any part of the Derivative Works; and 101 | 102 | (d) If the Work includes a "NOTICE" text file as part of its distribution, 103 | then any Derivative Works that You distribute must include a readable copy 104 | of the attribution notices contained within such NOTICE file, excluding 105 | those notices that do not pertain to any part of the Derivative Works, in at 106 | least one of the following places: within a NOTICE text file distributed as 107 | part of the Derivative Works; within the Source form or documentation, if 108 | provided along with the Derivative Works; or, within a display generated by 109 | the Derivative Works, if and wherever such third-party notices normally 110 | appear. The contents of the NOTICE file are for informational purposes only 111 | and do not modify the License. You may add Your own attribution notices 112 | within Derivative Works that You distribute, alongside or as an addendum to 113 | the NOTICE text from the Work, provided that such additional attribution 114 | notices cannot be construed as modifying the License. 115 | 116 | You may add Your own copyright statement to Your modifications and may 117 | provide additional or different license terms and conditions for use, 118 | reproduction, or distribution of Your modifications, or for any such 119 | Derivative Works as a whole, provided Your use, reproduction, and 120 | distribution of the Work otherwise complies with the conditions stated in 121 | this License. 122 | 123 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 124 | Contribution intentionally submitted for inclusion in the Work by You to the 125 | Licensor shall be under the terms and conditions of this License, without 126 | any additional terms or conditions. Notwithstanding the above, nothing 127 | herein shall supersede or modify the terms of any separate license agreement 128 | you may have executed with Licensor regarding such Contributions. 129 | 130 | 6. Trademarks. This License does not grant permission to use the trade names, 131 | trademarks, service marks, or product names of the Licensor, except as 132 | required for reasonable and customary use in describing the origin of the 133 | Work and reproducing the content of the NOTICE file. 134 | 135 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 136 | writing, Licensor provides the Work (and each Contributor provides its 137 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 138 | KIND, either express or implied, including, without limitation, any 139 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 140 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 141 | the appropriateness of using or redistributing the Work and assume any risks 142 | associated with Your exercise of permissions under this License. 143 | 144 | 8. Limitation of Liability. In no event and under no legal theory, whether in 145 | tort (including negligence), contract, or otherwise, unless required by 146 | applicable law (such as deliberate and grossly negligent acts) or agreed to 147 | in writing, shall any Contributor be liable to You for damages, including 148 | any direct, indirect, special, incidental, or consequential damages of any 149 | character arising as a result of this License or out of the use or inability 150 | to use the Work (including but not limited to damages for loss of goodwill, 151 | work stoppage, computer failure or malfunction, or any and all other 152 | commercial damages or losses), even if such Contributor has been advised of 153 | the possibility of such damages. 154 | 155 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 156 | Derivative Works thereof, You may choose to offer, and charge a fee for, 157 | acceptance of support, warranty, indemnity, or other liability obligations 158 | and/or rights consistent with this License. However, in accepting such 159 | obligations, You may act only on Your own behalf and on Your sole 160 | responsibility, not on behalf of any other Contributor, and only if You 161 | agree to indemnify, defend, and hold each Contributor harmless for any 162 | liability incurred by, or claims asserted against, such Contributor by 163 | reason of your accepting any such warranty or additional liability. 164 | 165 | END OF TERMS AND CONDITIONS 166 | 167 | APPENDIX: How to apply the Apache License to your work. 168 | 169 | To apply the Apache License to your work, attach the following 170 | boilerplate notice, with the fields enclosed by brackets "[]" 171 | replaced with your own identifying information. (Don't include 172 | the brackets!) The text should be enclosed in the appropriate 173 | comment syntax for the file format. We also recommend that a 174 | file or class name and description of purpose be included on the 175 | same "printed page" as the copyright notice for easier 176 | identification within third-party archives. 177 | 178 | Copyright 2022 CodeSandbox BV 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 181 | this file except in compliance with the License. You may obtain a copy of the 182 | License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software distributed 187 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 188 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 189 | specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Storybook CodeSandbox Addon 3 |

4 | 5 | # Storybook CodeSandbox Addon 6 | 7 | [![Edit in CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/github/codesandbox/storybook-addon) 8 | 9 | The `@codesandbox/storybook-addon` is a Storybook addon that allows users accessing a story-book page to export the code from the story as a CodeSandbox Sandbox. 10 | Sandboxes will always be created within the same workspace providing a closed-circuit feedback system. The add-on also provides support for private dependencies. 11 | 12 | ## Usage 13 | 14 | Once configured, you can use the "Open in CodeSandbox" button within your Storybook environment to export stories to CodeSandbox effortlessly. 15 | 16 | ## Configuration 17 | 18 | ```js 19 | // .storybook/main.js 20 | 21 | module.exports = { 22 | // ... 23 | addons: ["@codesandbox/storybook-addon"], 24 | }; 25 | ``` 26 | 27 |
28 | Storybook configuration (required) 29 | 30 |
31 | 32 | To run the addon, you'll need to configure it in your Storybook's `.storybook/preview.js` file. 33 | 34 | ```js 35 | // .storybook/preview.js 36 | 37 | const preview: Preview = { 38 | parameters: { 39 | codesandbox: { 40 | /** 41 | * @required 42 | * Workspace API key from codesandbox.io/t/permissions. 43 | * This sandbox is created inside the given workspace 44 | * and can be shared with team members. 45 | */ 46 | apiToken: "", 47 | 48 | /** 49 | * @optional 50 | * If a given sandbox id is provided, all other options 51 | * will be ignored and the addon will open the sandbox. 52 | */ 53 | sandboxId: "SANDBOX-ID", 54 | 55 | /** 56 | * @optional 57 | * Pass custom files/modules into the sandbox. These files 58 | * will be added to the file system of the sandbox and can 59 | * be imported by other files 60 | */ 61 | files: { 62 | // Example: 63 | "index.js": ` 64 | export const foo = () => console.log("Hello World");` 65 | "App.js": ` 66 | import { foo } from "./index.js"; 67 | 68 | foo();`, 69 | }, 70 | 71 | /** 72 | * @optional 73 | * Template preset to be used in the sandbox. This will 74 | * determine the initial setup of the sandbox, such as 75 | * bundler, dependencies, and files. 76 | */ 77 | template: "react" | "angular", // Defaults to "react" 78 | 79 | /** 80 | * @optional 81 | * Dependencies list to be installed in the sandbox. 82 | * 83 | * @note You cannot use local modules or packages since 84 | * this story runs in an isolated environment (sandbox) 85 | * inside CodeSandbox. As such, the sandbox doesn't have 86 | * access to your file system. 87 | * 88 | * Example: 89 | */ 90 | dependencies: { 91 | "@radix-ui/themes": "latest", 92 | "@myscope/mypackage": "1.0.0", 93 | }, 94 | 95 | /** 96 | * @required 97 | * CodeSandbox will try to import all components by default from 98 | * the given package, in case `mapComponent` property is not provided. 99 | * 100 | * This property is useful when your components imports are predictable 101 | * and come from a single package and entry point. 102 | */ 103 | fallbackImport: "@radix-ui/themes", 104 | 105 | /** 106 | * @optional 107 | * The default visibility of the new sandboxes inside the workspace. 108 | * 109 | * @note Use `private` if there is a private registry or private NPM 110 | * configured in your workspace. 111 | */ 112 | privacy: "private" | "public", 113 | 114 | /** 115 | * @optional 116 | * All required providers to run the sandbox properly, 117 | * such as themes, i18n, store, and so on. 118 | * 119 | * @note Remember to use only the dependencies listed above. 120 | * 121 | * Example: 122 | */ 123 | provider: `import { Theme } from "@radix-ui/themes"; 124 | import '@radix-ui/themes/styles.css'; 125 | 126 | export default ThemeProvider = ({ children }) => { 127 | return ( 128 | 129 | {children} 130 | 131 | ) 132 | }`, 133 | 134 | /** 135 | * @optional 136 | * Query parameters to be passed to the sandbox url that is opened. 137 | */ 138 | queryParams: { 139 | "file": "/src/App.js", 140 | }, 141 | }, 142 | }, 143 | }; 144 | 145 | export default preview; 146 | ``` 147 | 148 |
149 | 150 |
151 | Story configuration (recommended) 152 | 153 | ````ts 154 | import type { Meta, StoryObj } from "@storybook/react"; 155 | 156 | const meta: Meta = { 157 | title: "Example/Button", 158 | component: Button, 159 | parameters: { 160 | codesandbox: { 161 | /** 162 | * To import all components used within each story in 163 | * CodeSandbox, provide all necessary packages and modules. 164 | * 165 | * Given the following story: 166 | * ```js 167 | * import Provider from "@myscope/mypackage"; 168 | * import { Button } from "@radix-ui/themes"; 169 | * import "@radix-ui/themes/styles.css"; 170 | * ``` 171 | * 172 | * You need to map all imports to the following: 173 | */ 174 | mapComponent: { 175 | // Example of default imports 176 | "@myscope/mypackage": "Provider", 177 | 178 | // Example of named functions 179 | "@radix-ui/themes": ["Button"], 180 | 181 | // Example of static imports 182 | "@radix-ui/themes/styles.css": true, 183 | }, 184 | 185 | /** 186 | * @note You cannot use local modules or packages since 187 | * this story runs in an isolated environment (sandbox) 188 | * inside CodeSandbox. As such, the sandbox doesn't have 189 | * access to your file system. 190 | */ 191 | }, 192 | }, 193 | }; 194 | ```` 195 | 196 |
197 | 198 |
199 | 200 | Make sure to provide the necessary values for [`apiToken`](https://codesandbox.io/t/permissions) and any additional dependencies or providers required for your specific setup. 201 | 202 | For now, this addon only support [Component Story Format (CSF)](Component Story Format (CSF)) stories format. 203 | 204 | ## Additional Notes 205 | 206 | - Ensure that you have proper permissions and access rights to the CodeSandbox workspace specified in the configuration. 207 | - Verify the correctness of the dependencies and providers listed in the configuration to ensure the sandbox runs smoothly. 208 | -------------------------------------------------------------------------------- /manager.js: -------------------------------------------------------------------------------- 1 | import './dist/manager'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codesandbox/storybook-addon", 3 | "version": "0.2.2", 4 | "description": "CSB", 5 | "keywords": [ 6 | "storybook-addons" 7 | ], 8 | "repository": { 9 | "type": "git", 10 | "url": "github.com" 11 | }, 12 | "packageManager": "pnpm@9.15.0", 13 | "type": "module", 14 | "license": "Apache-2.0", 15 | "author": "CodeSandbox ", 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "require": "./dist/index.cjs", 20 | "import": "./dist/index.js" 21 | }, 22 | "./preset": "./dist/preset.cjs", 23 | "./manager": "./dist/manager.js", 24 | "./package.json": "./package.json" 25 | }, 26 | "files": [ 27 | "dist/**/*", 28 | "README.md", 29 | "*.js", 30 | "*.d.ts" 31 | ], 32 | "scripts": { 33 | "start": "pnpm run build && pnpm run storybook", 34 | "clean": "rimraf ./dist", 35 | "prebuild": "npm run clean", 36 | "build": "tsup", 37 | "build:watch": "npm run build -- --watch", 38 | "test": "echo \"Error: no test specified\" && exit 1", 39 | "prepublish": "zx scripts/prepublish-checks.js && npm run build", 40 | "eject-ts": "zx scripts/eject-typescript.js", 41 | "storybook": "storybook dev -p 6006", 42 | "build-storybook": "storybook build" 43 | }, 44 | "devDependencies": { 45 | "@radix-ui/themes": "^2.0.3", 46 | "@storybook/addon-essentials": "^7.6.6", 47 | "@storybook/addon-interactions": "^7.6.6", 48 | "@storybook/addon-links": "^7.6.6", 49 | "@storybook/blocks": "^7.6.6", 50 | "@storybook/core-events": "^7.6.6", 51 | "@storybook/manager": "^7.6.6", 52 | "@storybook/manager-api": "^7.6.6", 53 | "@storybook/preview": "^7.6.6", 54 | "@storybook/preview-api": "^7.6.6", 55 | "@storybook/react": "^7.6.6", 56 | "@storybook/react-vite": "^7.6.6", 57 | "@storybook/testing-library": "^0.2.2", 58 | "@storybook/theming": "^7.6.6", 59 | "@storybook/types": "^7.6.6", 60 | "@types/node": "^18.15.0", 61 | "@types/react": "^18.2.55", 62 | "@types/react-dom": "^18.2.19", 63 | "@vitejs/plugin-react": "^4.2.1", 64 | "boxen": "^7.1.1", 65 | "dedent": "^1.5.1", 66 | "npm-run-all": "^4.1.5", 67 | "prompts": "^2.4.2", 68 | "react": "^18.2.0", 69 | "react-dom": "^18.2.0", 70 | "rimraf": "^5.0.5", 71 | "storybook": "^7.6.6", 72 | "tsup": "^8.0.2", 73 | "typescript": "^5.3.3", 74 | "vite": "^5.1.2", 75 | "zx": "^7.2.3" 76 | }, 77 | "publishConfig": { 78 | "access": "public" 79 | }, 80 | "bundler": { 81 | "exportEntries": [ 82 | "src/index.ts" 83 | ], 84 | "managerEntries": [ 85 | "src/manager.tsx" 86 | ], 87 | "nodeEntries": [ 88 | "src/preset.ts" 89 | ] 90 | }, 91 | "storybook": { 92 | "displayName": "CodeSandbox", 93 | "supportedFrameworks": [ 94 | "react" 95 | ], 96 | "icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" 97 | }, 98 | "dependencies": { 99 | "@storybook/addon-docs": "^7.6.17", 100 | "@storybook/addon-storysource": "^7.6.17", 101 | "@storybook/docs-tools": "^7.6.17", 102 | "@storybook/icons": "^1.2.9", 103 | "prettier": "^3.2.5" 104 | } 105 | } -------------------------------------------------------------------------------- /preset.js: -------------------------------------------------------------------------------- 1 | // this file is slightly misleading. It needs to be CJS, and thus in this "type": "module" package it should be named preset.cjs 2 | // but Storybook won't pick that filename up so we have to name it preset.js instead 3 | 4 | module.exports = require('./dist/preset.cjs'); 5 | -------------------------------------------------------------------------------- /preview.js: -------------------------------------------------------------------------------- 1 | export * from './dist/preview'; 2 | -------------------------------------------------------------------------------- /scripts/eject-typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | // Copy TS files and delete src 4 | await $`cp -r ./src ./srcTS`; 5 | await $`rm -rf ./src`; 6 | await $`mkdir ./src`; 7 | 8 | // Install Babel and TS preset 9 | console.log(chalk.green` 10 | 11 | 🔃 Installing dependencies... 12 | 13 | `); 14 | await $`npm install --save-dev @babel/cli @babel/preset-typescript --ignore-scripts`; 15 | 16 | // Convert TS code to JS 17 | await $`babel --no-babelrc --presets @babel/preset-typescript ./srcTS -d ./src --extensions \".js,.jsx,.ts,.tsx\" --ignore "./srcTS/typings.d.ts"`; 18 | 19 | // Format the newly created .js files 20 | console.log(chalk.green` 21 | 22 | 💅 Format the newly created .js files... 23 | 24 | `); 25 | await $`prettier --write ./src`; 26 | 27 | // Add in minimal files required for the TS build setup 28 | console.log(chalk.green` 29 | 30 | ➕ Add minimal files required for the TS build setup 31 | 32 | `); 33 | await $`prettier --write ./src`; 34 | await $`touch ./src/dummy.ts`; 35 | await $`printf "export {};" >> ./src/dummy.ts`; 36 | 37 | await $`touch ./src/typings.d.ts`; 38 | await $`printf 'declare module "global";' >> ./src/typings.d.ts`; 39 | 40 | // Clean up 41 | await $`rm -rf ./srcTS`; 42 | console.log(chalk.green` 43 | 44 | 🧹 Clean up... 45 | 46 | `); 47 | await $`npm uninstall @babel/cli @babel/preset-typescript --ignore-scripts`; 48 | 49 | console.log( 50 | chalk.green.bold` 51 | TypeScript Ejection complete!`, 52 | chalk.green` 53 | Addon code converted with JS. The TypeScript build setup is still available in case you want to adopt TypeScript in the future. 54 | ` 55 | ); 56 | -------------------------------------------------------------------------------- /scripts/prepublish-checks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import boxen from "boxen"; 4 | import dedent from "dedent"; 5 | import { readFile } from 'fs/promises'; 6 | import { globalPackages as globalManagerPackages } from "@storybook/manager/globals"; 7 | import { globalPackages as globalPreviewPackages } from "@storybook/preview/globals"; 8 | 9 | const packageJson = await readFile('./package.json', 'utf8').then(JSON.parse); 10 | 11 | const name = packageJson.name; 12 | const displayName = packageJson.storybook.displayName; 13 | 14 | let exitCode = 0; 15 | $.verbose = false; 16 | 17 | /** 18 | * Check that meta data has been updated 19 | */ 20 | if (name.includes("addon-kit") || displayName.includes("Addon Kit")) { 21 | console.error( 22 | boxen( 23 | dedent` 24 | ${chalk.red.bold("Missing metadata")} 25 | 26 | ${chalk.red(dedent`Your package name and/or displayName includes default values from the Addon Kit. 27 | The addon gallery filters out all such addons. 28 | 29 | Please configure appropriate metadata before publishing your addon. For more info, see: 30 | https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata`)}`, 31 | { padding: 1, borderColor: "red" } 32 | ) 33 | ); 34 | 35 | exitCode = 1; 36 | } 37 | 38 | /** 39 | * Check that README has been updated 40 | */ 41 | const readmeTestStrings = 42 | "# Storybook Addon Kit|Click the \\*\\*Use this template\\*\\* button to get started.|https://user-images.githubusercontent.com/42671/106809879-35b32000-663a-11eb-9cdc-89f178b5273f.gif"; 43 | 44 | if ((await $`cat README.md | grep -E ${readmeTestStrings}`.exitCode) == 0) { 45 | console.error( 46 | boxen( 47 | dedent` 48 | ${chalk.red.bold("README not updated")} 49 | 50 | ${chalk.red(dedent`You are using the default README.md file that comes with the addon kit. 51 | Please update it to provide info on what your addon does and how to use it.`)} 52 | `, 53 | { padding: 1, borderColor: "red" } 54 | ) 55 | ); 56 | 57 | exitCode = 1; 58 | } 59 | 60 | /** 61 | * Check that globalized packages are not incorrectly listed as peer dependencies 62 | */ 63 | const peerDependencies = Object.keys(packageJson.peerDependencies || {}); 64 | const globalPackages = [...globalManagerPackages, ...globalPreviewPackages]; 65 | peerDependencies.forEach((dependency) => { 66 | if(globalPackages.includes(dependency)) { 67 | console.error( 68 | boxen( 69 | dedent` 70 | ${chalk.red.bold("Unnecessary peer dependency")} 71 | 72 | ${chalk.red(dedent`You have a peer dependency on ${chalk.bold(dependency)} which is most likely unnecessary 73 | as that is provided by Storybook directly. 74 | Check the "bundling" section in README.md for more information. 75 | If you are absolutely sure you are doing it correct, you should remove this check from scripts/prepublish-checks.js.`)} 76 | `, 77 | { padding: 1, borderColor: "red" } 78 | ) 79 | ); 80 | 81 | exitCode = 1; 82 | 83 | } 84 | }) 85 | 86 | process.exit(exitCode); 87 | -------------------------------------------------------------------------------- /scripts/welcome.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable no-console */ 3 | import prompts from 'prompts'; 4 | import { dedent } from 'ts-dedent'; 5 | import { dirname, resolve } from 'path'; 6 | import { readFile, writeFile } from 'fs/promises'; 7 | import { execSync } from 'child_process'; 8 | import { fileURLToPath } from 'url'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | // CLI questions 14 | const questions = [ 15 | { 16 | type: "text", 17 | name: "authorName", 18 | initial: "", 19 | message: "What is the package author name?*", 20 | validate: (name) => (name === "" ? "Name can't be empty" : true), 21 | }, 22 | { 23 | type: "text", 24 | name: "authorEmail", 25 | initial: "", 26 | message: "What is the package author email?", 27 | }, 28 | { 29 | type: "text", 30 | name: "packageName", 31 | message: "What is the addon package name (eg: storybook-addon-something)?*", 32 | validate: (name) => (name === "" ? "Package name can't be empty" : true), 33 | }, 34 | { 35 | type: "text", 36 | name: "displayName", 37 | message: 38 | "What is the addon display name (this will be used in the addon catalog)?*", 39 | validate: (name) => 40 | name === "" 41 | ? "Display name can't be empty. For more info, see: https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata" 42 | : true, 43 | }, 44 | { 45 | type: "text", 46 | name: "addonDescription", 47 | initial: "", 48 | message: "Write a short description of the addon*", 49 | validate: (name) => (name === "" ? "Description can't be empty" : true), 50 | }, 51 | { 52 | type: "text", 53 | name: "repoUrl", 54 | message: "Git repo URL for your addon package (https://github.com/...)*", 55 | validate: (url) => (url === "" ? "URL can't be empty" : true), 56 | }, 57 | { 58 | type: "text", 59 | name: "addonIcon", 60 | initial: 61 | "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png", 62 | message: "URL of your addon icon", 63 | }, 64 | { 65 | type: "list", 66 | name: "keywords", 67 | initial: "storybook-addons", 68 | message: "Enter addon keywords (comma separated)", 69 | separator: ",", 70 | format: (keywords) => 71 | keywords 72 | .concat(["storybook-addons"]) 73 | .map((k) => `"${k}"`) 74 | .join(", "), 75 | }, 76 | { 77 | type: "list", 78 | name: "supportedFrameworks", 79 | initial: 80 | "react, vue, angular, web-components, ember, html, svelte, preact, react-native", 81 | message: "List of frameworks you support (comma separated)?", 82 | separator: ",", 83 | format: (frameworks) => frameworks.map((k) => `"${k}"`).join(", "), 84 | }, 85 | ]; 86 | 87 | const REPLACE_TEMPLATES = { 88 | packageName: "storybook-addon-kit", 89 | addonDescription: "everything you need to build a Storybook addon", 90 | packageAuthor: "package-author", 91 | repoUrl: "https://github.com/storybookjs/storybook-addon-kit", 92 | keywords: `"storybook-addons"`, 93 | displayName: "Addon Kit", 94 | supportedFrameworks: `"supported-frameworks"`, 95 | }; 96 | 97 | const bold = (message) => `\u001b[1m${message}\u001b[22m`; 98 | const magenta = (message) => `\u001b[35m${message}\u001b[39m`; 99 | const blue = (message) => `\u001b[34m${message}\u001b[39m`; 100 | 101 | const main = async () => { 102 | console.log( 103 | bold( 104 | magenta( 105 | dedent` 106 | Welcome to Storybook addon-kit! 107 | Please answer the following questions while we prepare this project for you:\n 108 | ` 109 | ) 110 | ) 111 | ); 112 | 113 | const { 114 | authorName, 115 | authorEmail, 116 | packageName, 117 | addonDescription, 118 | repoUrl, 119 | displayName, 120 | keywords, 121 | supportedFrameworks, 122 | } = await prompts(questions); 123 | 124 | if (!authorName || !packageName) { 125 | console.log( 126 | `\nProcess canceled by the user. Feel free to run ${bold( 127 | "npm run postinstall" 128 | )} to execute the installation steps again!` 129 | ); 130 | process.exit(0); 131 | } 132 | 133 | const authorField = authorName + (authorEmail ? ` <${authorEmail}>` : ""); 134 | 135 | const packageJson = resolve(__dirname, `../package.json`); 136 | 137 | console.log(`\n👷 Updating package.json...`); 138 | let packageJsonContents = await readFile(packageJson, "utf-8"); 139 | 140 | packageJsonContents = packageJsonContents 141 | .replace(REPLACE_TEMPLATES.packageName, packageName) 142 | .replace(REPLACE_TEMPLATES.addonDescription, addonDescription) 143 | .replace(REPLACE_TEMPLATES.packageAuthor, authorField) 144 | .replace(REPLACE_TEMPLATES.keywords, keywords) 145 | .replace(REPLACE_TEMPLATES.repoUrl, repoUrl) 146 | .replace(REPLACE_TEMPLATES.displayName, displayName) 147 | .replace(REPLACE_TEMPLATES.supportedFrameworks, supportedFrameworks) 148 | .replace(/\s*"postinstall".*node.*scripts\/welcome.js.*",/, ''); 149 | 150 | await writeFile(packageJson, packageJsonContents); 151 | 152 | console.log("📝 Updating the README..."); 153 | const readme = resolve(__dirname, `../README.md`); 154 | let readmeContents = await readFile(readme, "utf-8"); 155 | 156 | const regex = /<\!-- README START -->([\s\S]*)<\!-- README END -->/g; 157 | 158 | readmeContents = readmeContents.replace( 159 | regex, 160 | dedent` 161 | # Storybook Addon ${displayName} 162 | ${addonDescription} 163 | ` 164 | ); 165 | 166 | await writeFile(readme, readmeContents); 167 | 168 | console.log(`📦 Creating a commit...`); 169 | execSync('git add . && git commit -m "project setup" --no-verify'); 170 | 171 | console.log( 172 | dedent`\n 173 | 🚀 All done! Run \`npm run start\` to get started. 174 | 175 | Thanks for using this template, ${authorName.split(" ")[0]}! ❤️ 176 | 177 | Feel free to open issues in case there are bugs/feature requests at: 178 | 179 | ${bold(blue("https://github.com/storybookjs/addon-kit"))}\n 180 | ` 181 | ); 182 | }; 183 | 184 | main().catch((e) => console.log(`Something went wrong: ${e}`)); 185 | -------------------------------------------------------------------------------- /src/Tool.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from "react"; 2 | import { API, useParameter, useStorybookApi } from "@storybook/manager-api"; 3 | import { BoxIcon } from "@storybook/icons"; 4 | import { IconButton } from "@storybook/components"; 5 | 6 | import { TOOL_ID } from "./constants"; 7 | import { convertSandboxToTemplate, parseFileTree, parseImports } from "./utils"; 8 | const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`; 9 | 10 | export type CSBParameters = 11 | | { 12 | apiToken: string; 13 | privacy?: "private" | "public"; 14 | fallbackImport?: string; 15 | mapComponent?: Record; 16 | dependencies?: Record; 17 | provider?: string; 18 | sandboxId: string; 19 | files: Record; 20 | template?: "react" | "angular"; 21 | queryParams?: Record; 22 | } 23 | | undefined; 24 | 25 | function createUrl( 26 | sandboxId: string, 27 | queryParams: Record, 28 | ): URL { 29 | const url = new URL(`https://codesandbox.io/p/sandbox/${sandboxId}`); 30 | url.searchParams.set("file", "/src/App.js"); 31 | for (const [key, value] of Object.entries(queryParams)) { 32 | url.searchParams.set(key, value); 33 | } 34 | url.searchParams.set("utm-source", "storybook-addon"); 35 | return url; 36 | } 37 | 38 | export const CodeSandboxTool = memo(function MyAddonSelector({ 39 | api, 40 | }: { 41 | api: API; 42 | }) { 43 | const { getCurrentStoryData, addNotification } = useStorybookApi(); 44 | const storyData = getCurrentStoryData(); 45 | const codesandboxParameters: CSBParameters = useParameter("codesandbox"); 46 | 47 | const [storySource, setStorySource] = useState(); 48 | const [loading, setLoading] = useState(false); 49 | 50 | useEffect(function getStorySourceCode() { 51 | api 52 | .getChannel() 53 | .on(SNIPPET_RENDERED, ({ source }) => setStorySource(source)); 54 | }, []); 55 | 56 | /** 57 | * Options 58 | */ 59 | const options = { 60 | template: codesandboxParameters?.template ?? "react", 61 | mapComponent: codesandboxParameters?.mapComponent ?? {}, 62 | dependencies: codesandboxParameters?.dependencies ?? {}, 63 | provider: 64 | codesandboxParameters?.provider ?? 65 | `export default GenericProvider = ({ children }) => { 66 | return children 67 | }`, 68 | files: codesandboxParameters?.files ?? {}, 69 | apiToken: codesandboxParameters?.apiToken, 70 | sandboxId: codesandboxParameters?.sandboxId, 71 | queryParams: codesandboxParameters?.queryParams ?? {}, 72 | }; 73 | 74 | async function createSandbox() { 75 | try { 76 | if (options.sandboxId) { 77 | window.open( 78 | createUrl(options.sandboxId, options.queryParams).toString(), 79 | "_blank", 80 | ); 81 | 82 | return; 83 | } 84 | 85 | setLoading(true); 86 | 87 | const { fallbackImport } = codesandboxParameters; 88 | const importsMap = options.mapComponent; 89 | 90 | // If fallbackImport is provided, add it to importsMap 91 | if (fallbackImport) { 92 | const componentNames = parseFileTree(storySource); 93 | 94 | // Check if fallbackImport is already in importsMap 95 | if (importsMap[fallbackImport]) { 96 | const currentFallbackImport = importsMap[fallbackImport]; 97 | 98 | // Merge them 99 | if (Array.isArray(currentFallbackImport)) { 100 | importsMap[fallbackImport] = [ 101 | ...new Set([...componentNames, ...currentFallbackImport]), 102 | ]; 103 | } else { 104 | // Invalid use case 105 | throw new Error( 106 | "Invalid fallback import usage. The `import` used inside `mapComponent` and also used as `fallbackImport` must be an array.", 107 | ); 108 | } 109 | } else { 110 | // Just added (0-config case) 111 | importsMap[fallbackImport] = componentNames; 112 | } 113 | } 114 | 115 | const imports = parseImports(importsMap); 116 | 117 | const files = await convertSandboxToTemplate({ 118 | ...options, 119 | imports, 120 | storySource, 121 | }); 122 | 123 | if (!codesandboxParameters.apiToken) { 124 | throw new Error("Missing `apiToken` property"); 125 | } 126 | 127 | const response = await fetch("https://api.codesandbox.io/sandbox", { 128 | method: "POST", 129 | body: JSON.stringify({ 130 | title: `${storyData.title} - Storybook`, 131 | files, 132 | privacy: codesandboxParameters.privacy === "public" ? 0 : 2, 133 | }), 134 | headers: { 135 | Authorization: `Bearer ${codesandboxParameters.apiToken}`, 136 | "Content-Type": "application/json", 137 | "X-CSB-API-Version": "2023-07-01", 138 | }, 139 | }); 140 | 141 | const data: { data: { alias: string } } = await response.json(); 142 | 143 | window.open( 144 | createUrl(data.data.alias, options.queryParams).toString(), 145 | "_blank", 146 | ); 147 | 148 | setLoading(false); 149 | } catch (error) { 150 | setLoading(false); 151 | 152 | addNotification({ 153 | content: { 154 | headline: "CodeSandbox: something went wrong", 155 | subHeadline: 156 | "Make sure you have a valid API token, or check the console for more details.", 157 | }, 158 | id: "csb-error", 159 | link: "", 160 | }); 161 | 162 | console.error(error.message); 163 | 164 | throw error; 165 | } 166 | } 167 | 168 | if (!codesandboxParameters || !storySource) { 169 | return; 170 | } 171 | 172 | return ( 173 | 185 | 186 | 187 | {loading ? "Loading..." : "Open in CodeSandbox"} 188 | 189 | ); 190 | }); 191 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADDON_ID = "storybook/codesandbox"; 2 | export const TOOL_ID = `${ADDON_ID}/tool`; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/manager.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { addons, types } from "@storybook/manager-api"; 3 | import { ADDON_ID, TOOL_ID } from "./constants"; 4 | import { CodeSandboxTool } from "./Tool"; 5 | 6 | addons.register(ADDON_ID, (api) => { 7 | addons.add(TOOL_ID, { 8 | type: types.TOOL, 9 | title: "CodeSandbox", 10 | match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), 11 | render: () => , 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/preset.ts: -------------------------------------------------------------------------------- 1 | export const viteFinal = async (config: any) => { 2 | return config; 3 | }; 4 | 5 | export const webpack = async (config: any) => { 6 | return config; 7 | }; 8 | -------------------------------------------------------------------------------- /src/stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import "@radix-ui/themes/styles.css"; 3 | 4 | import { Button } from "@radix-ui/themes"; 5 | 6 | const meta: Meta = { 7 | title: "Example/Button", 8 | component: Button, 9 | argTypes: { 10 | variant: { 11 | options: ["classic", "soft", "outline"], 12 | control: { type: "radio" }, 13 | }, 14 | size: { 15 | options: ["1", "2", "3", "4"], 16 | control: { type: "radio" }, 17 | }, 18 | }, 19 | parameters: { 20 | codesandbox: { 21 | template: "angular", 22 | files: { 23 | "/src/app/app.component.css": `button { 24 | }`, 25 | }, 26 | privacy: "private", 27 | mapComponent: { 28 | "@myscope/mypackage": "Provider", 29 | "@radix-ui/themes": ["Button"], 30 | "@radix-ui/themes/styles.css": true, 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | export default meta; 37 | type Story = StoryObj; 38 | 39 | export const Primary: Story = { 40 | args: { 41 | variant: "classic", 42 | children: "Button", 43 | }, 44 | }; 45 | 46 | export const Secondary: Story = { 47 | args: { 48 | variant: "soft", 49 | children: "Button", 50 | }, 51 | }; 52 | 53 | export const Large: Story = { 54 | args: { 55 | variant: "outline", 56 | size: "4", 57 | children: "Button", 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /src/stories/Text.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Source, } from '@storybook/blocks'; 2 | import * as TextStories from './Text.stories'; 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/stories/Text.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import "@radix-ui/themes/styles.css"; 4 | 5 | import { Text } from "@radix-ui/themes"; 6 | 7 | const meta: Meta = { 8 | component: Text, 9 | argTypes: { 10 | size: { 11 | options: ["1", "2", "3", "4"], 12 | control: { type: "radio" }, 13 | }, 14 | }, 15 | args: { 16 | children: "The quick brown fox jumps over the lazy dog.", 17 | }, 18 | }; 19 | 20 | export const Base: StoryObj = {}; 21 | 22 | export const Small: StoryObj = { 23 | args: { 24 | size: "1", 25 | }, 26 | parameters: { 27 | codesandbox: { 28 | mapComponent: { 29 | "@radix-ui/themes": ["Button"], 30 | "@radix-ui/themes/styles.css": true, 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | export const JSX = (props) => ( 37 | 38 | The quick brown fox jumps over the lazy dog. The quick brown fox jumps over 39 | the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown 40 | fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. 41 | The quick brown fox jumps over the lazy dog. The quick brown fox jumps over 42 | the lazy dog. The quick brown fox jumps over the lazy dog. 43 | 44 | ); 45 | 46 | export default meta; 47 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import prettier from "prettier/standalone"; 2 | import prettierPluginBabel from "prettier/plugins/babel"; 3 | import prettierPluginEstree from "prettier/plugins/estree"; 4 | import ts from "typescript"; 5 | 6 | import { CSBParameters } from "./Tool"; 7 | 8 | interface SandpackBundlerFile { 9 | code: string; 10 | hidden?: boolean; 11 | active?: boolean; 12 | readOnly?: boolean; 13 | } 14 | 15 | type SandpackBundlerFiles = Record; 16 | 17 | function standardizeFilesFormat( 18 | files: Record, 19 | ): SandpackBundlerFiles { 20 | return Object.entries(files).reduce((acc, [key, value]) => { 21 | if (typeof value === "string") { 22 | return { 23 | ...acc, 24 | [key]: { 25 | code: value, 26 | }, 27 | }; 28 | } 29 | 30 | return { ...acc, [key]: value }; 31 | }, {}); 32 | } 33 | 34 | export const parseImports = ( 35 | mapComponent: Required, 36 | ) => { 37 | let imports = ``; 38 | 39 | for (const [key, value] of Object.entries(mapComponent)) { 40 | if (Array.isArray(value)) { 41 | imports += `import { ${value.join(", ")} } from '${key}';\n`; 42 | } else if (value === true) { 43 | imports += `import '${key}';\n`; 44 | } else if (typeof value === "string") { 45 | imports += `import ${value} from '${key}';\n`; 46 | } 47 | } 48 | 49 | return imports; 50 | }; 51 | 52 | /** 53 | * Parse the source code with TreeSitter and return the component names 54 | */ 55 | export function parseFileTree(sourceCode: string): Array { 56 | const parsed = ts.createSourceFile( 57 | "story.tsx", 58 | sourceCode, 59 | ts.ScriptTarget.ESNext, 60 | ); 61 | 62 | const program = ts.createProgram({ 63 | rootNames: ["/story.tsx"], 64 | host: { 65 | readFile(_fileName) { 66 | return sourceCode; 67 | }, 68 | getSourceFile(_fileName) { 69 | return parsed; 70 | }, 71 | writeFile() {}, 72 | getDefaultLibFileName() { 73 | return "lib.d.ts"; 74 | }, 75 | useCaseSensitiveFileNames() { 76 | return false; 77 | }, 78 | getCurrentDirectory() { 79 | return "/"; 80 | }, 81 | getCanonicalFileName() { 82 | return "/story.tsx"; 83 | }, 84 | getNewLine() { 85 | return "\n"; 86 | }, 87 | fileExists() { 88 | return true; 89 | }, 90 | getDirectories() { 91 | return []; 92 | }, 93 | }, 94 | options: { 95 | target: ts.ScriptTarget.ESNext, 96 | module: ts.ModuleKind.ESNext, 97 | }, 98 | }); 99 | 100 | const checker = program.getTypeChecker(); 101 | 102 | const components: Array = []; 103 | 104 | function addTagName(tagName: ts.JsxTagNameExpression) { 105 | if (!ts.isIdentifier(tagName)) { 106 | return; 107 | } 108 | 109 | // Check if the variable is declared already in the source file, 110 | // we only want variables that are not already declared. 111 | const symbol = checker.getSymbolAtLocation(tagName); 112 | const declarations = symbol?.getDeclarations(); 113 | if (declarations) { 114 | return; 115 | } 116 | 117 | const componentName = tagName.getText(parsed); 118 | if (componentName[0].toUpperCase() === componentName[0]) { 119 | components.push(componentName); 120 | } 121 | } 122 | 123 | function walk(node: ts.Node) { 124 | if (ts.isJsxOpeningElement(node)) { 125 | addTagName(node.tagName); 126 | } else if (ts.isJsxSelfClosingElement(node)) { 127 | addTagName(node.tagName); 128 | } 129 | 130 | node.forEachChild((w) => walk(w)); 131 | } 132 | walk(parsed); 133 | 134 | return components; 135 | } 136 | 137 | type Props = { 138 | storySource: string; 139 | imports: string; 140 | } & CSBParameters; 141 | 142 | export function convertSandboxToTemplate( 143 | props: Props, 144 | ): Promise { 145 | switch (props.template) { 146 | case "react": 147 | return createReactTemplate(props); 148 | case "angular": 149 | return createAngularTemplate(props); 150 | default: 151 | return; 152 | } 153 | } 154 | 155 | async function createAngularTemplate( 156 | props: Props, 157 | ): Promise { 158 | const files = { 159 | "/src/app/app.component.css": { 160 | code: `body { 161 | font-family: sans-serif; 162 | -webkit-font-smoothing: auto; 163 | -moz-font-smoothing: auto; 164 | -moz-osx-font-smoothing: grayscale; 165 | font-smoothing: auto; 166 | text-rendering: optimizeLegibility; 167 | font-smooth: always; 168 | -webkit-tap-highlight-color: transparent; 169 | -webkit-touch-callout: none; 170 | } 171 | h1 { 172 | font-size: 1.5rem; 173 | }`, 174 | }, 175 | "/src/app/app.component.html": { 176 | code: `
177 |

{{ helloWorld }}

178 |
179 | `, 180 | }, 181 | "/src/app/app.component.ts": { 182 | code: `import { Component } from "@angular/core"; 183 | @Component({ 184 | selector: "app-root", 185 | templateUrl: "./app.component.html", 186 | styleUrls: ["./app.component.css"] 187 | }) 188 | export class AppComponent { 189 | helloWorld = "Hello world"; 190 | } 191 | `, 192 | }, 193 | "/src/app/app.module.ts": { 194 | code: `import { BrowserModule } from "@angular/platform-browser"; 195 | import { NgModule } from "@angular/core"; 196 | 197 | import { AppComponent } from "./app.component"; 198 | 199 | @NgModule({ 200 | declarations: [AppComponent], 201 | imports: [BrowserModule], 202 | providers: [], 203 | bootstrap: [AppComponent] 204 | }) 205 | export class AppModule {} 206 | `, 207 | }, 208 | "/src/index.html": { 209 | code: ` 210 | 211 | 212 | 213 | 214 | Angular 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | `, 227 | }, 228 | "/src/main.ts": { 229 | code: `import { enableProdMode } from "@angular/core"; 230 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; 231 | 232 | import { AppModule } from "./app/app.module"; 233 | platformBrowserDynamic() 234 | .bootstrapModule(AppModule) 235 | .catch(err => console.log(err)); 236 | 237 | `, 238 | }, 239 | "/src/polyfills.ts": { 240 | code: `import "core-js/proposals/reflect-metadata"; 241 | import "zone.js/dist/zone"; 242 | `, 243 | }, 244 | "/package.json": { 245 | code: JSON.stringify({ 246 | dependencies: { 247 | "@angular/core": "^11.2.0", 248 | "@angular/platform-browser": "^11.2.0", 249 | "@angular/platform-browser-dynamic": "^11.2.0", 250 | "@angular/common": "^11.2.0", 251 | "@angular/compiler": "^11.2.0", 252 | "zone.js": "0.11.3", 253 | "core-js": "3.8.3", 254 | rxjs: "6.6.3", 255 | ...props.dependencies, 256 | }, 257 | main: "/src/main.ts", 258 | }), 259 | }, 260 | 261 | ...standardizeFilesFormat(props.files), 262 | }; 263 | 264 | return files; 265 | } 266 | 267 | async function createReactTemplate( 268 | props: Props, 269 | ): Promise { 270 | const files = { 271 | "public/index.html": { 272 | code: ` 273 | 274 | 275 | 276 | 277 | Document 278 | 279 | 280 |
281 | 282 | `, 283 | }, 284 | "package.json": { 285 | code: JSON.stringify( 286 | { 287 | dependencies: { 288 | react: "^18.0.0", 289 | "react-dom": "^18.0.0", 290 | "react-scripts": "^5.0.0", 291 | ...props.dependencies, 292 | }, 293 | main: "/src/index.js", 294 | }, 295 | null, 296 | 2, 297 | ), 298 | }, 299 | "src/provider.js": { 300 | code: props.provider, 301 | }, 302 | "src/index.js": { 303 | code: `import React, { StrictMode } from "react"; 304 | import { createRoot } from "react-dom/client"; 305 | import GenericProvider from "./provider"; 306 | import App from "./App"; 307 | const root = createRoot(document.getElementById("root")); 308 | root.render( 309 | 310 | 311 | 312 | 313 | 314 | ); 315 | `, 316 | }, 317 | "src/App.js": { 318 | code: ` 319 | ${props.imports} 320 | export default App = () => { 321 | return ${props.storySource}; 322 | }`, 323 | }, 324 | ...standardizeFilesFormat(props.files), 325 | }; 326 | 327 | const prettifiedFiles: SandpackBundlerFiles = {}; 328 | const ignoredFileExtension = ["json", "html", "md"]; 329 | 330 | for (const [key, value] of Object.entries(files)) { 331 | if (ignoredFileExtension.includes(key.split(".").pop())) { 332 | prettifiedFiles[key] = value; 333 | 334 | continue; 335 | } 336 | 337 | prettifiedFiles[key] = { 338 | ...value, 339 | code: await prettier.format(value.code, { 340 | parser: "babel", 341 | plugins: [prettierPluginBabel, prettierPluginEstree], 342 | }), 343 | }; 344 | } 345 | 346 | return files; 347 | } 348 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "jsx": "react", 10 | "lib": ["es2020", "dom"], 11 | "module": "commonjs", 12 | "noImplicitAny": true, 13 | "rootDir": "./src", 14 | "skipLibCheck": true, 15 | "target": "ES2020" 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup"; 2 | import { readFile } from "fs/promises"; 3 | import { globalPackages as globalManagerPackages } from "@storybook/manager/globals"; 4 | import { globalPackages as globalPreviewPackages } from "@storybook/preview/globals"; 5 | 6 | // The current browsers supported by Storybook v7 7 | const BROWSER_TARGET: Options["target"] = [ 8 | "chrome100", 9 | "safari15", 10 | "firefox91", 11 | ]; 12 | const NODE_TARGET: Options["target"] = ["node18"]; 13 | 14 | type BundlerConfig = { 15 | bundler?: { 16 | exportEntries?: string[]; 17 | nodeEntries?: string[]; 18 | managerEntries?: string[]; 19 | previewEntries?: string[]; 20 | }; 21 | }; 22 | 23 | export default defineConfig(async (options) => { 24 | // reading the three types of entries from package.json, which has the following structure: 25 | // { 26 | // ... 27 | // "bundler": { 28 | // "exportEntries": ["./src/index.ts"], 29 | // "managerEntries": ["./src/manager.ts"], 30 | // "previewEntries": ["./src/preview.ts"] 31 | // "nodeEntries": ["./src/preset.ts"] 32 | // } 33 | // } 34 | const packageJson = (await readFile("./package.json", "utf8").then( 35 | JSON.parse, 36 | )) as BundlerConfig; 37 | const { 38 | bundler: { 39 | exportEntries = [], 40 | managerEntries = [], 41 | previewEntries = [], 42 | nodeEntries = [], 43 | } = {}, 44 | } = packageJson; 45 | 46 | const commonConfig: Options = { 47 | splitting: false, 48 | minify: !options.watch, 49 | treeshake: true, 50 | sourcemap: true, 51 | clean: true, 52 | }; 53 | 54 | const configs: Options[] = []; 55 | 56 | // export entries are entries meant to be manually imported by the user 57 | // they are not meant to be loaded by the manager or preview 58 | // they'll be usable in both node and browser environments, depending on which features and modules they depend on 59 | if (exportEntries.length) { 60 | configs.push({ 61 | ...commonConfig, 62 | entry: exportEntries, 63 | dts: { 64 | resolve: true, 65 | }, 66 | format: ["esm", "cjs"], 67 | target: [...BROWSER_TARGET, ...NODE_TARGET], 68 | platform: "neutral", 69 | external: [...globalManagerPackages, ...globalPreviewPackages], 70 | }); 71 | } 72 | 73 | // manager entries are entries meant to be loaded into the manager UI 74 | // they'll have manager-specific packages externalized and they won't be usable in node 75 | // they won't have types generated for them as they're usually loaded automatically by Storybook 76 | if (managerEntries.length) { 77 | configs.push({ 78 | ...commonConfig, 79 | entry: ["./src/manager.tsx"], 80 | format: ["esm"], 81 | target: BROWSER_TARGET, 82 | platform: "browser", 83 | external: globalManagerPackages, 84 | }); 85 | } 86 | 87 | // preview entries are entries meant to be loaded into the preview iframe 88 | // they'll have preview-specific packages externalized and they won't be usable in node 89 | // they'll have types generated for them so they can be imported when setting up Portable Stories 90 | if (previewEntries.length) { 91 | configs.push({ 92 | ...commonConfig, 93 | entry: previewEntries, 94 | dts: { 95 | resolve: true, 96 | }, 97 | format: ["esm"], 98 | target: BROWSER_TARGET, 99 | platform: "browser", 100 | external: globalPreviewPackages, 101 | }); 102 | } 103 | 104 | // node entries are entries meant to be used in node-only 105 | // this is useful for presets, which are loaded by Storybook when setting up configurations 106 | // they won't have types generated for them as they're usually loaded automatically by Storybook 107 | if (nodeEntries.length) { 108 | configs.push({ 109 | ...commonConfig, 110 | entry: nodeEntries, 111 | format: ["cjs"], 112 | target: NODE_TARGET, 113 | platform: "node", 114 | }); 115 | } 116 | 117 | return configs; 118 | }); 119 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------