├── .github └── workflows │ └── testing.yml ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── lerna.json ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── example-v8 │ ├── .storybook │ │ ├── main.js │ │ └── preview.js │ ├── package.json │ └── stories │ │ ├── BunnyDemo.js │ │ ├── DemosBasic.stories.js │ │ └── assets │ │ └── bunny.png ├── storybook-preset-vite │ ├── README.md │ ├── package.json │ ├── preset.js │ ├── src │ │ ├── index.ts │ │ └── types.ts │ └── tsconfig.json ├── storybook-preset-webpack │ ├── README.md │ ├── package.json │ ├── preset.js │ ├── src │ │ ├── index.ts │ │ └── types.ts │ └── tsconfig.json ├── storybook-renderer │ ├── README.md │ ├── package.json │ ├── preview.js │ ├── src │ │ ├── config.ts │ │ ├── globals.ts │ │ ├── helpers │ │ │ ├── PixiStory.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── render.ts │ │ ├── types │ │ │ ├── public-types.ts │ │ │ └── types.ts │ │ └── typings.d.ts │ └── tsconfig.json ├── storybook-vite │ ├── README.md │ ├── package.json │ ├── preset.js │ ├── preview.js │ ├── src │ │ ├── index.ts │ │ ├── preset.ts │ │ ├── types.ts │ │ └── typings.d.ts │ └── tsconfig.json └── storybook-webpack5 │ ├── README.md │ ├── package.json │ ├── preset.js │ ├── preview.js │ ├── src │ ├── index.ts │ ├── preset.ts │ ├── types.ts │ └── typings.d.ts │ └── tsconfig.json ├── scripts ├── prepare │ └── bundle.ts └── utils │ └── exec.ts └── tsconfig.json /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Use Node.js 11 | uses: actions/setup-node@v4 12 | with: 13 | node-version: "20" 14 | - run: npm ci 15 | - run: npm run build 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea/ 4 | .DS_store 5 | .vscode/ 6 | .nx -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.17.0 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2017 Mathew Groves, Chad Engler 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storybook for PixiJS 2 | 3 | --- 4 | 5 | Storybook for PixiJS is a UI development environment for your PixiJS Components. 6 | With it, you can visualize different states of your Components and develop them interactively. 7 | 8 | ![Storybook Screenshot](https://github.com/storybookjs/storybook/blob/main/media/storybook-intro.gif) 9 | 10 | Storybook runs outside of your app. 11 | So you can develop UI components in isolation without worrying about app specific dependencies and requirements. 12 | 13 | ## Getting Started 14 | 15 | We don't currently have a init script for PixiJS storybook. Currently the easiest way is 16 | to initialize with the html framework and manually edit the configuration: 17 | 18 | ```sh 19 | cd my-app 20 | npx storybook@"~8.0.0" init -t html 21 | ``` 22 | 23 | Remove HTML framework/renderer and install PixiJS framework/renderer based on your bunder: 24 | 25 | ```sh 26 | npm remove @storybook/html @storybook/html-webpack5 --save-dev 27 | ``` 28 | 29 | ``` 30 | npm install @pixi/storybook-renderer @pixi/storybook-webpack5 --save-dev 31 | ``` 32 | _or_ 33 | ``` 34 | npm install @pixi/storybook-renderer @pixi/storybook-vite --save-dev 35 | ``` 36 | 37 | Replace `.storybook/main.js` with the below, setting up the correct paths as necessary. 38 | 39 | ```javascript 40 | const config = { 41 | // use glob matching, eg: ../src/stories/**/*.stories.@(ts|tsx|js|jsx|mdx) 42 | stories: ['RELATIVE_PATH_TO_STORIES'], 43 | staticDirs: ['RELATIVE_PATH_TO_ASSETS'], 44 | logLevel: 'debug', 45 | addons: [ 46 | '@storybook/addon-actions', 47 | '@storybook/addon-backgrounds', 48 | '@storybook/addon-controls', 49 | '@storybook/addon-viewport', 50 | '@storybook/addon-links', 51 | '@storybook/addon-highlight', 52 | ], 53 | core: { 54 | channelOptions: { allowFunction: false, maxDepth: 10 }, 55 | disableTelemetry: true, 56 | }, 57 | features: { 58 | buildStoriesJson: true, 59 | breakingChangesV7: true, 60 | }, 61 | framework: '@pixi/storybook-webpack5', // or '@pixi/storybook-vite' 62 | }; 63 | 64 | export default config; 65 | ``` 66 | 67 | Replace `.storybook/preview.js` with: 68 | 69 | ```javascript 70 | const preview = { 71 | parameters: { 72 | layout: 'fullscreen', 73 | pixi: { 74 | // these are passed as options to `PIXI.Application` when instantiated by the 75 | // renderer 76 | applicationOptions: { 77 | backgroundColor: 0x1099bb, 78 | resolution: 1, 79 | }, 80 | // optional, if you want to provide custom resize logic, pass a function here, 81 | // if nothing is provided, the default resize function is used, which looks like 82 | // this, where w and h will be the width and height of the storybook canvas. 83 | resizeFn: (w, h) => { 84 | return { 85 | rendererWidth: w, 86 | rendererHeight: h, 87 | canvasWidth: w, 88 | canvasHeight: h, 89 | }; 90 | }, 91 | }, 92 | }, 93 | }; 94 | 95 | export default preview; 96 | ``` 97 | 98 | Depending on where you want to keep your story source, either delete `src/stories` folder 99 | if you plan to keep your stories co-located with their components, or empty `src/stories` 100 | of the example HTML stories and replace with your own. See below for instructions on 101 | how to write PixiJS Stories in the correct format. 102 | 103 | ## PixiJS Stories 104 | 105 | Unlike reactive web UI frameworks, PixiJS requires a more imperative code style to interact 106 | with the HTML5 Canvas API. Components are added to a display list, most commonly via a 107 | top level Application instance, and often need to respond to application-level events and 108 | callbacks. This Pixi renderer, handles setting up an Application and Renderer for you 109 | and handles adding and removing your Story's DisplayObject from the Stage, but in order to 110 | accommodate this, PixiJS stories must return a JS object with a fixed API: 111 | 112 | ```typescript 113 | type StoryFnPixiReturnType = { 114 | view: DisplayObject; 115 | // optionally respond to requestAnimationFrame tick 116 | update?: (delta: number) => void; 117 | // optionally respond to application level resizing 118 | resize?: (rendererWidth: number, rendererHeight: number) => void; 119 | // optionally clean up things when story is rerendered / removed - this happens a lot, so do it! 120 | destroy?: () => void; 121 | } 122 | ``` 123 | 124 | If your component already matches this particular interface, then you can just return an 125 | instance of it. 126 | 127 | You can see an [example story here](https://github.com/pixijs/pixi-storybook/tree/main/packages/example) 128 | 129 | ## Decorators 130 | 131 | PixiJS Storybook Framework is compatible with [Storybook Decorators](https://storybook.js.org/docs/react/writing-stories/decorators), 132 | these just need to return stories in the above API, for example you could add margin to a 133 | Story's component like so: 134 | 135 | ```javascript 136 | import { Container } from 'pixi.js'; 137 | 138 | const MARGIN = 10; 139 | const DOUBLE_MARGIN = MARGIN * 2; 140 | 141 | export const backgroundDecorator = (story) => { 142 | // The extra Container isn't really necessary just to add margin, but it shows how it's 143 | // possible to wrap with other PIXI objects 144 | const root = new Container(); 145 | 146 | // storyResult is the value returned from your Story function 147 | const storyResult = story(); 148 | 149 | root.addChild(storyResult.view); 150 | 151 | // Decorate the storyResult with other graphics / functionality: 152 | return { 153 | view: root, 154 | update: (delta) => storyResult.update?.(delta), 155 | resize: (screenWidth, screenHeight) => { 156 | root.x = MARGIN; 157 | root.y = MARGIN; 158 | 159 | storyResult.resize?.(screenWidth - DOUBLE_MARGIN, screenHeight - DOUBLE_MARGIN); 160 | }, 161 | destroy: () => storyResult.destroy?.(), 162 | }; 163 | }; 164 | ``` 165 | Then add the decorator to `./storybook/preview.js`: 166 | 167 | ```javascript 168 | import { backgroundDecorator } from 'decorators/backgroundDecorator'; 169 | 170 | // ... 171 | 172 | export const decorators = [backgroundDecorator]; 173 | ``` 174 | --- 175 | 176 | Storybook also comes with a lot of [addons](https://storybook.js.org/addons) and a great API to customize as you wish. 177 | You can also build a [static version](https://storybook.js.org/docs/html/sharing/publish-storybook) of your Storybook and deploy it anywhere you want. 178 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useWorkspaces": true, 4 | "version": "1.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx/tasks-runners/default", 5 | "options": { 6 | "cacheableOperations": [ 7 | "check", 8 | "build" 9 | ] 10 | } 11 | } 12 | }, 13 | "targetDefaults": { 14 | "check": { 15 | "dependsOn": [ 16 | "^check" 17 | ] 18 | }, 19 | "build": { 20 | "dependsOn": [ 21 | "^build" 22 | ], 23 | "outputs": [ 24 | "{projectRoot}/dist" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "build": "nx run-many --target=build --all --parallel", 9 | "build:force": "npm run build -- --skip-nx-cache", 10 | "prepublish": "npm run build:force", 11 | "check": "lerna run check", 12 | "publish": "lerna publish --no-private", 13 | "storybook": "nx run-many --target=storybook --all --parallel" 14 | }, 15 | "devDependencies": { 16 | "lerna": "^6.0.3", 17 | "@types/fs-extra": "^9.0.6", 18 | "fs-extra": "^9.0.1", 19 | "tsup": "^6.2.2", 20 | "esbuild-plugin-alias": "^0.2.1", 21 | "ts-dedent": "^2.0.0", 22 | "ts-node": "^10.4.0", 23 | "typescript": "~5.3.3", 24 | "slash": "^3.0.0", 25 | "execa": "^5.0.0", 26 | "chalk": "^4.1.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v8/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx|mdx)'], 3 | staticDirs: ['../stories/assets'], 4 | logLevel: 'debug', 5 | addons: [ 6 | '@storybook/addon-actions', 7 | '@storybook/addon-backgrounds', 8 | '@storybook/addon-controls', 9 | '@storybook/addon-viewport', 10 | '@storybook/addon-links', 11 | '@storybook/addon-highlight', 12 | ], 13 | core: { 14 | channelOptions: { allowFunction: false, maxDepth: 10 }, 15 | disableTelemetry: true, 16 | }, 17 | features: { 18 | buildStoriesJson: true, 19 | breakingChangesV7: true, 20 | }, 21 | framework: '@pixi/storybook-webpack5', 22 | }; 23 | -------------------------------------------------------------------------------- /packages/example-v8/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | layout: 'fullscreen', 3 | pixi: { 4 | applicationOptions: { 5 | backgroundColor: 0x1099bb, 6 | resolution: 1, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/example-v8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-v8", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "storybook": "storybook dev -p 6006" 7 | }, 8 | "dependencies": { 9 | "@storybook/addon-essentials": "^8.0.0", 10 | "@storybook/addon-interactions": "^8.0.0", 11 | "@storybook/addon-links": "^8.0.0", 12 | "@pixi/storybook-renderer": "*", 13 | "@pixi/storybook-vite": "*", 14 | "@pixi/storybook-webpack5": "*", 15 | "pixi.js": "^8.0.0", 16 | "storybook": "^8.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/example-v8/stories/BunnyDemo.js: -------------------------------------------------------------------------------- 1 | import { Assets, Container, Sprite } from "pixi.js"; 2 | 3 | export class BunnyDemo { 4 | constructor({ bunnySize, bunnySpacing, someInjectedObject }, appReady) { 5 | this.view = new Container(); 6 | 7 | appReady.then(async () => { 8 | const tex = await Assets.load("bunny.png"); 9 | const numBunnies = bunnySize * bunnySize; 10 | 11 | for (let i = 0; i < numBunnies; i += 1) { 12 | const bunny = new Sprite(tex); 13 | bunny.buttonMode = true; 14 | bunny.interactive = true; 15 | bunny.on("pointerdown", someInjectedObject.onBunnyClick); 16 | bunny.x = (i % bunnySize) * bunnySpacing; 17 | bunny.y = Math.floor(i / bunnySize) * bunnySpacing; 18 | this.view.addChild(bunny); 19 | } 20 | 21 | // Center bunny sprite in local container coordinates 22 | this.view.pivot.x = this.view.width / 2; 23 | this.view.pivot.y = this.view.height / 2; 24 | }); 25 | } 26 | 27 | resize(w, h) { 28 | this.view.x = w / 2; 29 | this.view.y = h / 2; 30 | } 31 | 32 | update(ticker) { 33 | this.view.rotation -= 0.01 * ticker.deltaTime; 34 | } 35 | 36 | destroy() { 37 | console.log("destroying bunny demo"); 38 | this.view.destroy(true); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/example-v8/stories/DemosBasic.stories.js: -------------------------------------------------------------------------------- 1 | import { action } from "@storybook/addon-actions"; 2 | 3 | import { BunnyDemo } from "./BunnyDemo"; 4 | 5 | export default { 6 | title: "Demos-Basic", 7 | args: { 8 | bunnySize: 5, 9 | bunnySpacing: 40, 10 | someInjectedObject: { 11 | onBunnyClick: action("onBunnyClick"), 12 | }, 13 | }, 14 | }; 15 | 16 | export const Default = { 17 | render: (args, ctx) => { 18 | return new BunnyDemo(args, ctx.parameters.pixi.appReady); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/example-v8/stories/assets/bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixijs/storybook/bcb0a911fb41dc48e0423feef2c672d97e9058c1/packages/example-v8/stories/assets/bunny.png -------------------------------------------------------------------------------- /packages/storybook-preset-vite/README.md: -------------------------------------------------------------------------------- 1 | # Storybook Vite preset for PixiJS 2 | 3 | Storybook PixiJS Vite preset 4 | -------------------------------------------------------------------------------- /packages/storybook-preset-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi/storybook-preset-vite", 3 | "version": "1.0.0", 4 | "description": "Storybook for PixiJS: View PixiJS Components in isolation with Hot Reloading.", 5 | "homepage": "https://github.com/pixijs/storybook/tree/main/packages/storybook-preset-vite", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pixijs/storybook.git", 9 | "directory": "packages/storybook-preset-vite" 10 | }, 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs", 16 | "types": "./dist/index.d.ts" 17 | }, 18 | "./preset": { 19 | "require": "./dist/index.js", 20 | "import": "./dist/index.mjs", 21 | "types": "./dist/index.d.ts" 22 | }, 23 | "./package.json": { 24 | "require": "./package.json", 25 | "import": "./package.json", 26 | "types": "./package.json" 27 | } 28 | }, 29 | "main": "dist/index.js", 30 | "module": "dist/index.mjs", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist/**/*", 34 | "README.md", 35 | "*.js", 36 | "*.d.ts" 37 | ], 38 | "scripts": { 39 | "check": "tsc --noEmit", 40 | "build": "../../scripts/prepare/bundle.ts" 41 | }, 42 | "dependencies": { 43 | "@storybook/builder-vite": "^8.0.0", 44 | "@storybook/types": "^8.0.0", 45 | "@types/node": "^18.0.0", 46 | "react": "16.14.0", 47 | "react-dom": "16.14.0" 48 | }, 49 | "devDependencies": { 50 | "typescript": "~5.3.3" 51 | }, 52 | "engines": { 53 | "node": ">=18" 54 | }, 55 | "publishConfig": { 56 | "access": "public" 57 | }, 58 | "bundler": { 59 | "entries": [ 60 | "./src/index.ts" 61 | ], 62 | "platform": "node" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/storybook-preset-vite/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index'); 2 | -------------------------------------------------------------------------------- /packages/storybook-preset-vite/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfigVite } from "./types"; 2 | 3 | export * from "./types"; 4 | 5 | export const vite: StorybookConfigVite["viteFinal"] = (config) => { 6 | return config; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/storybook-preset-vite/src/types.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | StorybookConfigVite, 3 | BuilderOptions, 4 | } from "@storybook/builder-vite"; 5 | export type { StorybookConfig, TypescriptOptions } from "@storybook/types"; 6 | -------------------------------------------------------------------------------- /packages/storybook-preset-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "skipLibCheck": true 6 | }, 7 | "include": ["src/**/*"], 8 | } 9 | -------------------------------------------------------------------------------- /packages/storybook-preset-webpack/README.md: -------------------------------------------------------------------------------- 1 | # Storybook Webpack preset for PixiJS 2 | 3 | Storybook PixiJS Webpack preset 4 | -------------------------------------------------------------------------------- /packages/storybook-preset-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi/storybook-preset-webpack", 3 | "version": "1.0.0", 4 | "description": "Storybook for PixiJS: View PixiJS Components in isolation with Hot Reloading.", 5 | "homepage": "https://github.com/pixijs/storybook/tree/main/packages/storybook-preset-webpack", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pixijs/storybook.git", 9 | "directory": "packages/storybook-preset-webpack" 10 | }, 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs", 16 | "types": "./dist/index.d.ts" 17 | }, 18 | "./preset": { 19 | "require": "./dist/index.js", 20 | "import": "./dist/index.mjs", 21 | "types": "./dist/index.d.ts" 22 | }, 23 | "./package.json": { 24 | "require": "./package.json", 25 | "import": "./package.json", 26 | "types": "./package.json" 27 | } 28 | }, 29 | "main": "dist/index.js", 30 | "module": "dist/index.mjs", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist/**/*", 34 | "README.md", 35 | "*.js", 36 | "*.d.ts" 37 | ], 38 | "scripts": { 39 | "check": "tsc --noEmit", 40 | "build": "../../scripts/prepare/bundle.ts" 41 | }, 42 | "dependencies": { 43 | "@storybook/core-webpack": "^8.0.0", 44 | "@types/node": "^18.0.0", 45 | "webpack": "5" 46 | }, 47 | "devDependencies": { 48 | "typescript": "~5.3.3" 49 | }, 50 | "peerDependencies": { 51 | "@babel/core": "*" 52 | }, 53 | "engines": { 54 | "node": ">=18" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | }, 59 | "bundler": { 60 | "entries": [ 61 | "./src/index.ts" 62 | ], 63 | "platform": "node" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/storybook-preset-webpack/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index'); 2 | -------------------------------------------------------------------------------- /packages/storybook-preset-webpack/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from './types'; 2 | 3 | export * from './types'; 4 | 5 | export const webpack: StorybookConfig['webpack'] = (config) => { 6 | const rules = [ 7 | ...(config.module?.rules || []), 8 | { 9 | test: /\.(png|jpg|gif|woff|woff2)$/, 10 | type: 'asset/inline', 11 | }, 12 | { 13 | test: /\.(mp4|ogg|svg)$/, 14 | type: 'asset/resource', 15 | }, 16 | { 17 | test: /\.(glsl|frag|vert|wgsl)$/, 18 | type: 'asset/source', 19 | }, 20 | ]; 21 | 22 | // eslint-disable-next-line no-param-reassign 23 | config.module = config.module || {}; 24 | // eslint-disable-next-line no-param-reassign 25 | config.module.rules = rules; 26 | 27 | return config; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/storybook-preset-webpack/src/types.ts: -------------------------------------------------------------------------------- 1 | export type { BuilderResult, TypescriptOptions, StorybookConfig } from '@storybook/core-webpack'; 2 | -------------------------------------------------------------------------------- /packages/storybook-preset-webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "skipLibCheck": true 6 | }, 7 | "include": ["src/**/*"], 8 | } 9 | -------------------------------------------------------------------------------- /packages/storybook-renderer/README.md: -------------------------------------------------------------------------------- 1 | # Storybook PixiJS Renderer 2 | 3 | Storybook PixiJS Renderer 4 | -------------------------------------------------------------------------------- /packages/storybook-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi/storybook-renderer", 3 | "version": "1.0.0", 4 | "description": "Storybook PixiJS renderer", 5 | "homepage": "https://github.com/pixijs/storybook/tree/main/packages/storybook-renderer", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pixijs/storybook.git", 9 | "directory": "packages/storybook-renderer" 10 | }, 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs", 16 | "types": "./dist/index.d.ts" 17 | }, 18 | "./preview": { 19 | "require": "./dist/config.js", 20 | "import": "./dist/config.mjs", 21 | "types": "./dist/config.d.ts" 22 | }, 23 | "./package.json": { 24 | "require": "./package.json", 25 | "import": "./package.json", 26 | "types": "./package.json" 27 | } 28 | }, 29 | "main": "dist/index.js", 30 | "module": "dist/index.mjs", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist/**/*", 34 | "README.md", 35 | "*.js", 36 | "*.d.ts" 37 | ], 38 | "scripts": { 39 | "check": "tsc --emitDeclarationOnly false --noEmit", 40 | "build": "../../scripts/prepare/bundle.ts && tsc" 41 | }, 42 | "dependencies": { 43 | "@storybook/csf": "~0.1.2", 44 | "@storybook/docs-tools": "^8.0.0", 45 | "@storybook/manager-api": "^8.0.0", 46 | "@storybook/preview-api": "^8.0.0", 47 | "deep-equal": "^2.2.3", 48 | "global": "^4.4.0", 49 | "ts-dedent": "^2.2.0" 50 | }, 51 | "devDependencies": { 52 | "@types/deep-equal": "^1.0.4", 53 | "@types/webpack-env": "^1.18.4", 54 | "pixi.js": "^8.0.0", 55 | "typescript": "~5.3.3" 56 | }, 57 | "peerDependencies": { 58 | "@babel/core": "*", 59 | "pixi.js": "^7.0.0 || ^8.0.0" 60 | }, 61 | "engines": { 62 | "node": ">=18" 63 | }, 64 | "publishConfig": { 65 | "access": "public" 66 | }, 67 | "bundler": { 68 | "entries": [ 69 | "./src/index.ts", 70 | "./src/config.ts" 71 | ], 72 | "platform": "browser" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/storybook-renderer/preview.js: -------------------------------------------------------------------------------- 1 | export * from './dist/config'; 2 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/config.ts: -------------------------------------------------------------------------------- 1 | export const parameters = { framework: 'pixi' as const }; 2 | export { renderToDOM, renderToCanvas } from './render'; 3 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/globals.ts: -------------------------------------------------------------------------------- 1 | import global from 'global'; 2 | 3 | const { window: globalWindow } = global; 4 | 5 | globalWindow.STORYBOOK_ENV = 'PIXI'; 6 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/helpers/PixiStory.ts: -------------------------------------------------------------------------------- 1 | import { Container, Ticker } from 'pixi.js'; 2 | import { StoryFn } from '../types/public-types'; 3 | 4 | export class PixiStory { 5 | public view: Container; 6 | public update!: (ticker: Ticker | number) => void; 7 | public resize!: (width: number, height: number) => void; 8 | public destroy!: () => void; 9 | 10 | constructor(options: { 11 | context: Parameters>[1]; 12 | init: (view: Container) => void; 13 | update?: (view: Container, ticker?: Ticker | number) => void; 14 | resize?: (view: Container, width: number, height: number) => void; 15 | destroy?: (view: Container) => void; 16 | }) { 17 | this.view = new Container(); 18 | 19 | options.context.parameters.pixi.appReady.then(() => { 20 | options.init(this.view); 21 | }); 22 | 23 | if (options.update !== undefined) { 24 | this.update = (ticker: Ticker | number) => { 25 | options.update!(this.view, ticker); 26 | }; 27 | } 28 | 29 | if (options.resize !== undefined) { 30 | this.resize = (width: number, height: number) => { 31 | options.resize!(this.view, width, height); 32 | }; 33 | } 34 | 35 | if (options.destroy !== undefined) { 36 | this.destroy = () => { 37 | options.destroy!(this.view); 38 | }; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PixiStory'; 2 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import './globals'; 4 | 5 | export * from './types/public-types'; 6 | export * from './types/types'; 7 | export * from './helpers'; 8 | 9 | // optimization: stop HMR propagation in webpack 10 | module?.hot?.decline(); 11 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/render.ts: -------------------------------------------------------------------------------- 1 | import equals from 'deep-equal'; 2 | import type { ApplicationOptions } from 'pixi.js'; 3 | import { Application, Ticker } from 'pixi.js'; 4 | 5 | import type { RenderContext, RenderToCanvas } from '@storybook/types'; 6 | import { dedent } from 'ts-dedent'; 7 | import type { 8 | ApplicationResizeFunction, 9 | ApplicationResizeFunctionReturnType, 10 | EventHandler, 11 | PixiFramework, 12 | StoryFnPixiReturnType, 13 | } from './types/types'; 14 | 15 | let pixiApp: Application; 16 | let canvas = document.createElement('canvas'); 17 | canvas.style.display = 'block'; 18 | let appReady: Promise; 19 | let lastApplicationOptions: ApplicationOptions; 20 | let storyState: { 21 | resizeHandler: EventHandler; 22 | storyObject: StoryFnPixiReturnType; 23 | } | null = null; 24 | const resizeState = { 25 | w: 0, 26 | h: 0, 27 | canvasWidth: 0, 28 | canvasHeight: 0, 29 | }; 30 | 31 | function updater() { 32 | let first = false; 33 | return function (ticker: Ticker){ 34 | if (first) { 35 | resizeApplication({ 36 | containerWidth: window.innerWidth, 37 | containerHeight: window.innerHeight, 38 | app: pixiApp, 39 | resizeFn: resizeDefault, 40 | force: true, 41 | }); 42 | } 43 | appReady.then(() => { 44 | storyState?.storyObject?.update?.(ticker); 45 | }); 46 | }; 47 | } 48 | 49 | function resizeDefault(w: number, h: number): ApplicationResizeFunctionReturnType { 50 | return { 51 | rendererWidth: w, 52 | rendererHeight: h, 53 | canvasWidth: w, 54 | canvasHeight: h, 55 | }; 56 | } 57 | 58 | function getPixiApplication(applicationOptions: ApplicationOptions): Application { 59 | // Destroy previous app instance and create a new one each time applicationOptions 60 | // changes - in theory it shouldn't be often 61 | if (!equals(applicationOptions, lastApplicationOptions)) { 62 | if (pixiApp) { 63 | pixiApp.destroy(true); 64 | } 65 | 66 | // check if Application has init method 67 | if (!Application.prototype.init) { 68 | pixiApp = new Application({ ...applicationOptions, view: canvas }); 69 | appReady = Promise.resolve(); 70 | } else { 71 | pixiApp = new Application(); 72 | appReady = pixiApp.init({ ...applicationOptions, canvas }); 73 | } 74 | 75 | lastApplicationOptions = applicationOptions; 76 | } 77 | 78 | return pixiApp; 79 | } 80 | 81 | function resizeApplication({ 82 | containerWidth, 83 | containerHeight, 84 | app, 85 | resizeFn, 86 | storyObject, 87 | force = false, 88 | }: { 89 | containerWidth: number; 90 | containerHeight: number; 91 | app: Application; 92 | resizeFn: ApplicationResizeFunction; 93 | storyObject?: StoryFnPixiReturnType; 94 | force?: boolean; 95 | }) { 96 | const { renderer } = app; 97 | const newSize = resizeFn(containerWidth, containerHeight); 98 | if ( 99 | force || 100 | resizeState.w !== newSize.rendererWidth || 101 | resizeState.h !== newSize.rendererHeight || 102 | resizeState.canvasWidth !== newSize.canvasWidth || 103 | resizeState.canvasHeight !== newSize.canvasHeight 104 | ) { 105 | resizeState.w = newSize.rendererWidth; 106 | resizeState.h = newSize.rendererHeight; 107 | resizeState.canvasWidth = newSize.canvasWidth; 108 | resizeState.canvasHeight = newSize.canvasHeight; 109 | canvas.style.width = `${newSize.canvasWidth}px`; 110 | canvas.style.height = `${newSize.canvasHeight}px`; 111 | window.scrollTo(0, 0); 112 | if (renderer) renderer.resize(resizeState.w, resizeState.h); 113 | storyObject?.resize?.(resizeState.w, resizeState.h); 114 | } 115 | } 116 | 117 | function initResize({ 118 | app, 119 | resizeFn, 120 | storyObject, 121 | }: { 122 | app: Application; 123 | resizeFn: ApplicationResizeFunction; 124 | storyObject?: StoryFnPixiReturnType; 125 | }): EventHandler { 126 | const storyResizeHandler = (e: Event) => 127 | resizeApplication({ 128 | containerWidth: window.innerWidth, 129 | containerHeight: window.innerHeight, 130 | app, 131 | resizeFn, 132 | storyObject, 133 | }); 134 | 135 | // TODO: throttle/debounce? 136 | window.addEventListener('resize', storyResizeHandler); 137 | // Manually call resize each story render, use force to ensure `storyResizeFn` is called 138 | // if it exists, since story component will be recreated 139 | appReady.then(() => { 140 | resizeApplication({ 141 | containerWidth: window.innerWidth, 142 | containerHeight: window.innerHeight, 143 | app, 144 | resizeFn, 145 | storyObject, 146 | force: Boolean(storyObject?.resize), 147 | }); 148 | }); 149 | 150 | return storyResizeHandler; 151 | } 152 | 153 | let updateRef: (ticker: Ticker) => void; 154 | function addStory({ 155 | app, 156 | resizeFn, 157 | storyObject, 158 | }: { 159 | app: Application; 160 | resizeFn: ApplicationResizeFunction; 161 | storyObject: StoryFnPixiReturnType; 162 | }): EventHandler { 163 | const storyResizeHandler = initResize({ 164 | app, 165 | resizeFn, 166 | storyObject, 167 | }); 168 | 169 | app.stage.addChild(storyObject.view); 170 | 171 | if (storyObject.update) { 172 | updateRef = updater(); 173 | Ticker.shared.add(updateRef); 174 | } 175 | 176 | return storyResizeHandler; 177 | } 178 | 179 | function removeStory({ 180 | app, 181 | storyObject, 182 | storyResizeHandler, 183 | }: { 184 | app: Application; 185 | storyObject: StoryFnPixiReturnType; 186 | storyResizeHandler: EventHandler; 187 | }) { 188 | if (storyObject.update) { 189 | Ticker.shared.remove(updateRef); 190 | } 191 | 192 | app.stage.removeChild(storyObject.view); 193 | 194 | window.removeEventListener('resize', storyResizeHandler); 195 | 196 | storyObject.destroy?.(); 197 | } 198 | 199 | export const renderToDOM: RenderToCanvas = ( 200 | { storyContext, unboundStoryFn, kind, id, name, showMain, showError, forceRemount }: RenderContext, 201 | domElement: unknown, 202 | ) => { 203 | const canvasElement = domElement as HTMLCanvasElement; 204 | 205 | const { parameters } = storyContext; 206 | const { pixi: pixiParameters } = parameters; 207 | const { applicationOptions, resizeFn = resizeDefault } = pixiParameters; 208 | 209 | // Create a new PIXI.Application instance each time applicationOptions changes, ideally 210 | // applicationOptions is set globally in pixi parameters in `.storybook/preview.ts`, but 211 | // it's possible to override this on a per-story basis if needed 212 | // TODO: recreate PIXI.Application if forceRemount is true? 213 | const app = getPixiApplication(applicationOptions); 214 | 215 | // Expose app to a global variable for debugging using `pixi-inspector` (https://github.com/bfanger/pixi-inspector) 216 | (globalThis as any).__PIXI_APP__ = app; 217 | 218 | if (canvasElement.firstChild !== canvas || forceRemount) { 219 | canvasElement.innerHTML = ''; 220 | canvasElement.appendChild(canvas); 221 | } 222 | 223 | if (storyState) { 224 | // Each story rerender (basically when any args are changed in Storybook's Controls 225 | // Panel), remove and kill the last story's PIXI component instance, but keep the same 226 | // PIXI Application instance alive. We'll recreate a new component instance with any 227 | // changed args. This is different to how a reactive framework would work, but is 228 | // necessary for PIXI's imperative style 229 | removeStory({ 230 | app, 231 | storyObject: storyState.storyObject, 232 | storyResizeHandler: storyState.resizeHandler, 233 | }); 234 | storyState = null; 235 | } 236 | 237 | const storyObject = unboundStoryFn({ 238 | ...storyContext, 239 | parameters: { 240 | ...parameters, 241 | pixi: { 242 | ...pixiParameters, 243 | app, 244 | appReady, 245 | }, 246 | }, 247 | }); 248 | showMain(); 249 | 250 | if (!storyObject.view) { 251 | showError({ 252 | title: `Expecting a StoryFnPixiReturnType from the story: "${name}" of "${kind}".`, 253 | description: dedent` 254 | Did you forget to return the correct object from the story? 255 | `, 256 | }); 257 | return () => {}; 258 | } 259 | 260 | const storyResizeHandler = addStory({ app, resizeFn, storyObject }); 261 | storyState = { 262 | storyObject, 263 | resizeHandler: storyResizeHandler, 264 | }; 265 | 266 | return () => { 267 | // This cleanup function only runs when a story is unloaded, not after a rerender: 268 | // Remove and kill this story, see storyState check above for handling rerenders 269 | removeStory({ app, storyObject, storyResizeHandler }); 270 | }; 271 | } 272 | 273 | export const renderToCanvas: RenderToCanvas = renderToDOM; -------------------------------------------------------------------------------- /packages/storybook-renderer/src/types/public-types.ts: -------------------------------------------------------------------------------- 1 | import type { Args, ComponentAnnotations, StoryAnnotations, AnnotatedStoryFn } from '@storybook/csf'; 2 | import type { PixiFramework } from './types'; 3 | 4 | export type { Args, ArgTypes, Parameters } from '@storybook/csf'; 5 | 6 | /** 7 | * Metadata to configure the stories for a component. 8 | * 9 | * @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export) 10 | */ 11 | export type Meta = ComponentAnnotations; 12 | 13 | /** 14 | * Story function that represents a CSFv2 component example. 15 | * 16 | * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) 17 | */ 18 | export type StoryFn = AnnotatedStoryFn; 19 | 20 | /** 21 | * Story function that represents a CSFv3 component example. 22 | * 23 | * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) 24 | */ 25 | export type StoryObj = StoryAnnotations; 26 | 27 | /** 28 | * Story function that represents a CSFv3 component example. 29 | * 30 | * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) 31 | */ 32 | export type Story = StoryObj; 33 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/types/types.ts: -------------------------------------------------------------------------------- 1 | import type { StoryContext as DefaultStoryContext, Renderer } from '@storybook/csf'; 2 | import { Application, ApplicationOptions, Container, Ticker } from 'pixi.js'; 3 | 4 | export type { RenderContext } from '@storybook/types'; 5 | 6 | export type ApplicationResizeFunctionReturnType = { 7 | rendererWidth: number; 8 | rendererHeight: number; 9 | canvasWidth: number; 10 | canvasHeight: number; 11 | }; 12 | export type ApplicationResizeFunction = (w: number, h: number) => ApplicationResizeFunctionReturnType; 13 | 14 | export type PixiRendererParameters = { 15 | applicationOptions: ApplicationOptions; 16 | resizeFn: ApplicationResizeFunction; 17 | app: Application; 18 | appReady: Promise; 19 | }; 20 | 21 | export type EventHandler = (e: Event) => void; 22 | 23 | export type StoryResizeFn = (w: number, h: number) => void; 24 | 25 | export type StoryFnPixiReturnType = { 26 | view: Container; 27 | update?: (ticker: Ticker) => void; 28 | resize?: StoryResizeFn; 29 | destroy?: () => void; 30 | }; 31 | 32 | export interface IStorybookStory { 33 | name: string; 34 | render: (context: any) => any; 35 | } 36 | 37 | export interface IStorybookSection { 38 | kind: string; 39 | stories: IStorybookStory[]; 40 | } 41 | 42 | export interface ShowErrorArgs { 43 | title: string; 44 | description: string; 45 | } 46 | 47 | export interface PixiFramework extends Renderer { 48 | component: Container; 49 | storyResult: StoryFnPixiReturnType; 50 | } 51 | 52 | export type StoryContext = DefaultStoryContext & { 53 | parameters: DefaultStoryContext['parameters'] & { 54 | pixi: PixiRendererParameters; 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/storybook-renderer/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'global'; 2 | -------------------------------------------------------------------------------- /packages/storybook-renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "emitDeclarationOnly": true, 6 | "declaration": true, 7 | "outDir": "dist", 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true 10 | }, 11 | "include": ["src/**/*"], 12 | } 13 | -------------------------------------------------------------------------------- /packages/storybook-vite/README.md: -------------------------------------------------------------------------------- 1 | # Storybook PixiJS Framework 2 | 3 | Storybook PixiJS Framework 4 | -------------------------------------------------------------------------------- /packages/storybook-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi/storybook-vite", 3 | "version": "1.0.0", 4 | "description": "Storybook for PixiJS: View Pixi Components in isolation with Hot Reloading.", 5 | "homepage": "https://github.com/pixijs/storybook/tree/main/packages/storybook-vite", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pixijs/storybook.git", 9 | "directory": "packages/storybook-vite" 10 | }, 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs", 16 | "types": "./dist/index.d.ts" 17 | }, 18 | "./preset": { 19 | "require": "./dist/preset.js", 20 | "import": "./dist/preset.mjs", 21 | "types": "./dist/preset.d.ts" 22 | }, 23 | "./package.json": { 24 | "require": "./package.json", 25 | "import": "./package.json", 26 | "types": "./package.json" 27 | } 28 | }, 29 | "main": "dist/index.js", 30 | "module": "dist/index.mjs", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist/**/*", 34 | "README.md", 35 | "*.js", 36 | "*.d.ts" 37 | ], 38 | "scripts": { 39 | "check": "tsc --noEmit", 40 | "build": "../../scripts/prepare/bundle.ts" 41 | }, 42 | "dependencies": { 43 | "@pixi/storybook-preset-vite": "^1.0.0", 44 | "@pixi/storybook-renderer": "^1.0.0", 45 | "@storybook/builder-vite": "^8.0.0", 46 | "@storybook/core-common": "^8.0.0", 47 | "@types/node": "^18.0.0", 48 | "@types/offscreencanvas": "^2019.7.0", 49 | "global": "^4.4.0", 50 | "react": "16.14.0", 51 | "react-dom": "16.14.0" 52 | }, 53 | "devDependencies": { 54 | "typescript": "~5.3.3" 55 | }, 56 | "engines": { 57 | "node": ">=18" 58 | }, 59 | "publishConfig": { 60 | "access": "public" 61 | }, 62 | "bundler": { 63 | "entries": [ 64 | "./src/index.ts", 65 | "./src/preset.ts" 66 | ], 67 | "platform": "node" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/storybook-vite/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/preset'); 2 | -------------------------------------------------------------------------------- /packages/storybook-vite/preview.js: -------------------------------------------------------------------------------- 1 | export * from './dist/config'; 2 | -------------------------------------------------------------------------------- /packages/storybook-vite/src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export * from '@pixi/storybook-renderer'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/storybook-vite/src/preset.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import type { PresetProperty, PresetPropertyFn } from "@storybook/types"; 3 | import type { StorybookConfig } from "./types"; 4 | 5 | export const addons: PresetProperty<"addons", StorybookConfig> = [ 6 | path.dirname( 7 | require.resolve(path.join("@pixi/storybook-preset-vite", "package.json")) 8 | ), 9 | path.dirname( 10 | require.resolve(path.join("@pixi/storybook-renderer", "package.json")) 11 | ), 12 | ]; 13 | 14 | export const core: PresetPropertyFn<"core", StorybookConfig> = async ( 15 | config, 16 | options 17 | ) => { 18 | const framework = await options.presets.apply( 19 | "framework" 20 | ); 21 | 22 | return { 23 | ...config, 24 | builder: { 25 | name: path.dirname( 26 | require.resolve(path.join("@storybook/builder-vite", "package.json")) 27 | ) as "@storybook/builder-vite", 28 | options: 29 | typeof framework === "string" ? {} : framework.options.builder || {}, 30 | }, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/storybook-vite/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BuilderOptions, 3 | StorybookConfig as StorybookConfigBase, 4 | StorybookConfigVite, 5 | TypescriptOptions as TypescriptOptionsBuilder, 6 | } from "../../storybook-preset-vite/src/types"; 7 | 8 | type FrameworkName = "@pixi/storybook-vite"; 9 | type BuilderName = "@storybook/builder-vite"; 10 | 11 | export type FrameworkOptions = { 12 | builder?: BuilderOptions; 13 | }; 14 | 15 | type StorybookConfigFramework = { 16 | framework: 17 | | FrameworkName 18 | | { 19 | name: FrameworkName; 20 | options: FrameworkOptions; 21 | }; 22 | frameworkPath?: string; 23 | core?: StorybookConfigBase["core"] & { 24 | builder?: 25 | | BuilderName 26 | | { 27 | name: BuilderName; 28 | options: BuilderOptions; 29 | }; 30 | }; 31 | typescript?: Partial & 32 | StorybookConfigBase["typescript"]; 33 | }; 34 | 35 | /** 36 | * The interface for Storybook configuration in `main.ts` files. 37 | */ 38 | export type StorybookConfig = Omit< 39 | StorybookConfigBase, 40 | keyof StorybookConfigVite | keyof StorybookConfigFramework 41 | > & 42 | StorybookConfigVite & 43 | StorybookConfigFramework; 44 | -------------------------------------------------------------------------------- /packages/storybook-vite/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'global'; 2 | 3 | // will be provided by the webpack define plugin 4 | declare var NODE_ENV: string | undefined; 5 | -------------------------------------------------------------------------------- /packages/storybook-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "skipLibCheck": true 6 | }, 7 | "include": ["src/**/*"], 8 | } 9 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/README.md: -------------------------------------------------------------------------------- 1 | # Storybook PixiJS Framework 2 | 3 | Storybook PixiJS Framework 4 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pixi/storybook-webpack5", 3 | "version": "1.0.0", 4 | "description": "Storybook for PixiJS: View Pixi Components in isolation with Hot Reloading.", 5 | "homepage": "https://github.com/pixijs/storybook/tree/main/packages/storybook-webpack5", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pixijs/storybook.git", 9 | "directory": "packages/storybook-webpack5" 10 | }, 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs", 16 | "types": "./dist/index.d.ts" 17 | }, 18 | "./preset": { 19 | "require": "./dist/preset.js", 20 | "import": "./dist/preset.mjs", 21 | "types": "./dist/preset.d.ts" 22 | }, 23 | "./package.json": { 24 | "require": "./package.json", 25 | "import": "./package.json", 26 | "types": "./package.json" 27 | } 28 | }, 29 | "main": "dist/index.js", 30 | "module": "dist/index.mjs", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist/**/*", 34 | "README.md", 35 | "*.js", 36 | "*.d.ts" 37 | ], 38 | "scripts": { 39 | "check": "tsc --noEmit", 40 | "build": "../../scripts/prepare/bundle.ts" 41 | }, 42 | "dependencies": { 43 | "@pixi/storybook-preset-webpack": "^1.0.0", 44 | "@pixi/storybook-renderer": "^1.0.0", 45 | "@storybook/addon-webpack5-compiler-babel": "^3.0.0", 46 | "@storybook/builder-webpack5": "^8.0.0", 47 | "@storybook/core-common": "^8.0.0", 48 | "@types/node": "^18.0.0", 49 | "@types/offscreencanvas": "^2019.7.3", 50 | "global": "^4.4.0" 51 | }, 52 | "devDependencies": { 53 | "typescript": "~5.3.3" 54 | }, 55 | "peerDependencies": { 56 | "@babel/core": "*" 57 | }, 58 | "engines": { 59 | "node": ">=18" 60 | }, 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "bundler": { 65 | "entries": [ 66 | "./src/index.ts", 67 | "./src/preset.ts" 68 | ], 69 | "platform": "node" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/preset'); 2 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/preview.js: -------------------------------------------------------------------------------- 1 | export * from './dist/config'; 2 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export * from '@pixi/storybook-renderer'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/src/preset.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { PresetProperty, PresetPropertyFn } from '@storybook/types'; 3 | import type { StorybookConfig } from './types'; 4 | 5 | export const addons: PresetProperty<'addons', StorybookConfig> = [ 6 | path.dirname(require.resolve(path.join('@pixi/storybook-preset-webpack', 'package.json'))), 7 | path.dirname(require.resolve(path.join('@pixi/storybook-renderer', 'package.json'))), 8 | ]; 9 | 10 | export const core: PresetPropertyFn<'core', StorybookConfig> = async (config, options) => { 11 | const framework = await options.presets.apply('framework'); 12 | 13 | return { 14 | ...config, 15 | builder: { 16 | name: path.dirname( 17 | require.resolve(path.join('@storybook/builder-webpack5', 'package.json')), 18 | ) as '@storybook/builder-webpack5', 19 | options: typeof framework === 'string' ? {} : framework.options.builder || {}, 20 | }, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | StorybookConfig as StorybookConfigBase, 3 | TypescriptOptions as TypescriptOptionsReact, 4 | } from '@pixi/storybook-preset-webpack'; 5 | import type { 6 | StorybookConfigWebpack, 7 | BuilderOptions, 8 | TypescriptOptions as TypescriptOptionsBuilder, 9 | } from '@storybook/builder-webpack5'; 10 | 11 | type FrameworkName = '@pixi/storybook-webpack5'; 12 | type BuilderName = '@storybook/builder-webpack5'; 13 | 14 | export type FrameworkOptions = { 15 | builder?: BuilderOptions; 16 | }; 17 | 18 | type StorybookConfigFramework = { 19 | framework: 20 | | FrameworkName 21 | | { 22 | name: FrameworkName; 23 | options: FrameworkOptions; 24 | }; 25 | frameworkPath?: string; 26 | core?: StorybookConfigBase['core'] & { 27 | builder?: 28 | | BuilderName 29 | | { 30 | name: BuilderName; 31 | options: BuilderOptions; 32 | }; 33 | }; 34 | typescript?: Partial & StorybookConfigBase['typescript']; 35 | }; 36 | 37 | /** 38 | * The interface for Storybook configuration in `main.ts` files. 39 | */ 40 | export type StorybookConfig = Omit & 41 | StorybookConfigWebpack & 42 | StorybookConfigFramework; 43 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'global'; 2 | 3 | // will be provided by the webpack define plugin 4 | declare var NODE_ENV: string | undefined; 5 | -------------------------------------------------------------------------------- /packages/storybook-webpack5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "skipLibCheck": true 6 | }, 7 | "include": ["src/**/*"], 8 | } 9 | -------------------------------------------------------------------------------- /scripts/prepare/bundle.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ../../node_modules/.bin/ts-node 2 | 3 | import fs from 'fs-extra'; 4 | import path, { dirname, join, relative } from 'path'; 5 | import { build } from 'tsup'; 6 | import aliasPlugin from 'esbuild-plugin-alias'; 7 | import dedent from 'ts-dedent'; 8 | import slash from 'slash'; 9 | import { exec } from '../utils/exec'; 10 | 11 | const hasFlag = (flags: string[], name: string) => !!flags.find((s) => s.startsWith(`--${name}`)); 12 | 13 | const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { 14 | const { 15 | name, 16 | dependencies, 17 | peerDependencies, 18 | bundler: { entries, platform, pre, post }, 19 | } = await fs.readJson(join(cwd, 'package.json')); 20 | 21 | const tsnodePath = join(cwd, '..', '..', 'node_modules', '.bin', 'ts-node'); 22 | 23 | if (pre) { 24 | await exec(`${tsnodePath} ${pre}`, { cwd }); 25 | } 26 | 27 | const reset = hasFlag(flags, 'reset'); 28 | const watch = hasFlag(flags, 'watch'); 29 | const optimized = hasFlag(flags, 'optimized'); 30 | 31 | if (reset) { 32 | await fs.emptyDir(join(process.cwd(), 'dist')); 33 | } 34 | 35 | if (!optimized) { 36 | await Promise.all( 37 | entries.map(async (file: string) => { 38 | console.log(`skipping generating types for ${file}`); 39 | const { name: entryName, dir } = path.parse(file); 40 | 41 | const pathName = join(process.cwd(), dir.replace('./src', 'dist'), `${entryName}.d.ts`); 42 | const srcName = join(process.cwd(), file); 43 | 44 | const rel = relative(dirname(pathName), dirname(srcName)).split(path.sep).join(path.posix.sep); 45 | 46 | await fs.ensureFile(pathName); 47 | await fs.writeFile( 48 | pathName, 49 | dedent` 50 | // devmode 51 | export * from '${rel}/${entryName}'; 52 | `, 53 | ); 54 | }), 55 | ); 56 | } 57 | 58 | const tsConfigPath = join(cwd, 'tsconfig.json'); 59 | const tsConfigExists = await fs.pathExists(tsConfigPath); 60 | await Promise.all([ 61 | build({ 62 | entry: entries.map((e: string) => slash(join(cwd, e))), 63 | watch, 64 | ...(tsConfigExists ? { tsconfig: tsConfigPath } : {}), 65 | outDir: join(process.cwd(), 'dist'), 66 | // sourcemap: optimized, 67 | format: ['esm'], 68 | target: 'chrome100', 69 | clean: !watch, 70 | platform: platform || 'browser', 71 | esbuildPlugins: [ 72 | aliasPlugin({ 73 | process: path.resolve('../node_modules/process/browser.js'), 74 | util: path.resolve('../node_modules/util/util.js'), 75 | }), 76 | ], 77 | external: [name, ...Object.keys(dependencies || {}), ...Object.keys(peerDependencies || {})], 78 | 79 | dts: 80 | optimized && tsConfigExists 81 | ? { 82 | entry: entries, 83 | resolve: true, 84 | } 85 | : false, 86 | esbuildOptions: (c) => { 87 | /* eslint-disable no-param-reassign */ 88 | c.conditions = ['module']; 89 | c.define = optimized 90 | ? { 91 | 'process.env.NODE_ENV': "'production'", 92 | 'process.env': '{}', 93 | global: 'window', 94 | } 95 | : { 96 | 'process.env.NODE_ENV': "'development'", 97 | 'process.env': '{}', 98 | global: 'window', 99 | }; 100 | c.platform = platform || 'browser'; 101 | c.legalComments = 'none'; 102 | c.minifyWhitespace = optimized; 103 | c.minifyIdentifiers = optimized; 104 | c.minifySyntax = optimized; 105 | /* eslint-enable no-param-reassign */ 106 | }, 107 | }), 108 | build({ 109 | entry: entries.map((e: string) => slash(join(cwd, e))), 110 | watch, 111 | outDir: join(process.cwd(), 'dist'), 112 | ...(tsConfigExists ? { tsconfig: tsConfigPath } : {}), 113 | format: ['cjs'], 114 | target: 'node16', 115 | platform: 'node', 116 | clean: !watch, 117 | external: [name, ...Object.keys(dependencies || {}), ...Object.keys(peerDependencies || {})], 118 | 119 | esbuildOptions: (c) => { 120 | /* eslint-disable no-param-reassign */ 121 | c.platform = 'node'; 122 | c.legalComments = 'none'; 123 | c.minifyWhitespace = optimized; 124 | c.minifyIdentifiers = optimized; 125 | c.minifySyntax = optimized; 126 | /* eslint-enable no-param-reassign */ 127 | }, 128 | }), 129 | ]); 130 | 131 | if (post) { 132 | await exec(`${tsnodePath} ${post}`, { cwd }, { debug: true }); 133 | } 134 | }; 135 | 136 | const flags = process.argv.slice(2); 137 | const cwd = process.cwd(); 138 | 139 | run({ cwd, flags }).catch((err: unknown) => { 140 | // We can't let the stack try to print, it crashes in a way that sets the exit code to 0. 141 | // Seems to have something to do with running JSON.parse() on binary / base64 encoded sourcemaps 142 | // in @cspotcode/source-map-support 143 | if (err instanceof Error) { 144 | console.error(err.message); 145 | } 146 | process.exit(1); 147 | }); 148 | -------------------------------------------------------------------------------- /scripts/utils/exec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop, no-restricted-syntax */ 2 | import type { ExecaChildProcess, Options } from 'execa'; 3 | import execa from 'execa'; 4 | import chalk from 'chalk'; 5 | 6 | const logger = console; 7 | 8 | type StepOptions = { 9 | startMessage?: string; 10 | errorMessage?: string; 11 | dryRun?: boolean; 12 | debug?: boolean; 13 | signal?: AbortSignal; 14 | }; 15 | 16 | export const exec = async ( 17 | command: string | string[], 18 | options: Options = {}, 19 | { startMessage, errorMessage, dryRun, debug, signal }: StepOptions = {}, 20 | ): Promise => { 21 | logger.info(); 22 | if (startMessage) logger.info(startMessage); 23 | 24 | if (dryRun) { 25 | logger.info(`\n> ${command}\n`); 26 | return undefined; 27 | } 28 | 29 | const defaultOptions: Options = { 30 | shell: true, 31 | stdout: debug ? 'inherit' : 'pipe', 32 | stderr: debug ? 'inherit' : 'pipe', 33 | }; 34 | let currentChild: ExecaChildProcess; 35 | 36 | // Newer versions of execa have explicit support for abort signals, but this works 37 | if (signal) { 38 | signal.addEventListener('abort', () => currentChild.kill()); 39 | } 40 | 41 | try { 42 | if (typeof command === 'string') { 43 | logger.debug(`> ${command}`); 44 | currentChild = execa.command(command, { ...defaultOptions, ...options }); 45 | await currentChild; 46 | } else { 47 | for (const subcommand of command) { 48 | logger.debug(`> ${subcommand}`); 49 | currentChild = execa.command(subcommand, { ...defaultOptions, ...options }); 50 | await currentChild; 51 | } 52 | } 53 | } catch (err) { 54 | if (!err.killed) { 55 | logger.error(chalk.red(`An error occurred while executing: \`${command}\``)); 56 | logger.log(`${errorMessage}\n`); 57 | } 58 | 59 | throw err; 60 | } 61 | 62 | return undefined; 63 | }; 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "incremental": false, 6 | "noImplicitAny": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "moduleResolution": "Node", 10 | "target": "ES2020", 11 | "module": "CommonJS", 12 | "skipLibCheck": false, 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "isolatedModules": true, 16 | "strictBindCallApply": true, 17 | "lib": ["dom", "esnext"], 18 | "paths": { 19 | "@pixi/storybook-renderer": ["packages/storybook-renderer/src"], 20 | "@pixi/storybook-preset-vite": ["packages/storybook-preset-vite/src"], 21 | "@pixi/storybook-preset-webpack": ["packages/storybook-preset-webpack/src"], 22 | "@pixi/storybook-vite": ["packages/storybook-vite/src"], 23 | "@pixi/storybook-webpack5": ["packages/storybook-webpack5/src"], 24 | } 25 | }, 26 | "exclude": ["dist", "**/dist", "node_modules", "**/node_modules", "**/setup-jest.ts"], 27 | "ts-node": { 28 | "transpileOnly": true, 29 | "files": true, 30 | "compilerOptions": { 31 | "types": ["node"] 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------