3 |6 | 7 | -------------------------------------------------------------------------------- /pnpm-publish-summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedPackages": [ 3 | { 4 | "name": "@storybook-vue/nuxt", 5 | "version": "0.1.0-rc.10" 6 | }, 7 | { 8 | "name": "storybook-nuxt", 9 | "version": "0.1.0-rc.10" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/storybook-nuxt/src/runtime/composables/router.ts: -------------------------------------------------------------------------------- 1 | import { useNuxtApp } from 'nuxt/app' 2 | import { useRouter as useVueRouter } from 'vue-router' 3 | 4 | export function useRouter() { 5 | const router = useNuxtApp()?.$router ?? useVueRouter() 6 | return router 7 | } 8 | -------------------------------------------------------------------------------- /packages/storybook-nuxt/playground/stories/types.ts: -------------------------------------------------------------------------------- 1 | // types.ts 2 | export interface Props { 3 | /** 4 | * description for prop "a" type definiton 5 | * in external file . 6 | * @file ./types.ts 7 | * @default "Hello World" 8 | * */ 9 | 10 | a: string 11 | } 12 | -------------------------------------------------------------------------------- /packages/storybook-nuxt/playground/components/MyComposable.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |Hello
4 |Storybook-Nuxt World 5 |
{{ JSON.stringify(config, null, 2) }}
9 |
10 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/playground/pages/index.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | {{ route.path }}
40 |
37 |
38 | ## v0.1.5
39 |
40 | [compare changes](https://github.com/storybook-vue/storybook-nuxt/compare/v0.1.4...v0.1.5)
41 |
42 | ### 🩹 Fixes
43 |
44 | - Fix error "No builder configured in core." ([888ce4c](https://github.com/storybook-vue/storybook-nuxt/commit/888ce4c))
45 |
46 | ### 🏡 Chore
47 |
48 | - **release:** V0.1.4 ([5d92c87](https://github.com/storybook-vue/storybook-nuxt/commit/5d92c87))
49 |
50 | ### ❤️ Contributors
51 |
52 | - Tobias Diez
53 | - ChakAs3 ([@chakAs3](http://github.com/chakAs3))
54 |
55 | ## v0.1.4
56 |
57 | [compare changes](https://github.com/storybook-vue/storybook-nuxt/compare/v0.1.4...v0.1.4)
58 |
59 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/playground/assets/button.css:
--------------------------------------------------------------------------------
1 | .storybook {
2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
3 | font-size: 14px;
4 | line-height: 1.5;
5 | color: #333;
6 | background-color: white;
7 | }
8 |
9 | .storybook-button {
10 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
11 | font-weight: 700;
12 | border: 0;
13 | border-radius: 3em;
14 | cursor: pointer;
15 | display: inline-block;
16 | line-height: 1;
17 | }
18 | .storybook-button--primary {
19 | color: white;
20 | background-color: #1ea7fd;
21 | }
22 | .storybook-button--secondary {
23 | color: #333;
24 | background-color: transparent;
25 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
26 | }
27 | .storybook-button--small {
28 | font-size: 12px;
29 | padding: 10px 16px;
30 | }
31 | .storybook-button--medium {
32 | font-size: 14px;
33 | padding: 11px 20px;
34 | }
35 | .storybook-button--large {
36 | font-size: 16px;
37 | padding: 12px 24px;
38 | }
39 | .with-border {
40 | border: 1px solid #d8adad;
41 | border-radius: 6px;
42 | padding: 10px;
43 | }
44 | .sb-row {
45 | display: flex;
46 | gap: 20px;
47 | align-items: center;
48 | border: 1px solid #d8adad;
49 | border-radius: 6px;
50 | padding: 10px;
51 | }
52 |
53 | .sb-column {
54 | display: flex;
55 | flex-direction: column;
56 | align-items: flex-start;
57 | justify-content: center;
58 | }
59 | .lang-selector {
60 | display: flex;
61 | align-items: left;
62 | justify-content: left;
63 | gap: 10px;
64 | padding: 20px 0px;
65 | }
66 | .welcome {
67 | display: flex;
68 | align-items: left;
69 | flex-direction: column;
70 | justify-content: left;
71 | gap: 10px;
72 | font-size: large;
73 | width: 100%;
74 | padding: 20px 0px;
75 | }
76 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/.storybook/rendererAssets/common/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@storybook-nuxt/monorepo",
3 | "version": "0.0.1",
4 | "packageManager": "pnpm@8.6.12",
5 | "description": "Storybook for Nuxt and Vite: Develop Vue3 components in isolation with Hot Reloading.",
6 | "keywords": [
7 | "storybook",
8 | "nuxt",
9 | "vite",
10 | "vue3"
11 | ],
12 | "scripts": {
13 | "build": "pnpm -r --filter=\"./packages/**/*\" run build",
14 | "stub": "pnpm -r run stub",
15 | "cleanup": "rimraf 'packages/**/node_modules' 'node_modules'",
16 | "dev": "pnpm run stub && pnpm -C packages/storybook-nuxt dev",
17 | "dev:cli": "pnpm run stub && pnpm -C packages/storybook-nuxt-cli dev",
18 | "test:cli": "pnpm -C packages/storybook-nuxt-cli test",
19 | "lint": "eslint .",
20 | "release": "pnpm -r publish",
21 | "test": "pnpm lint",
22 | "docs": "nuxi dev docs",
23 | "typecheck": "vue-tsc --noEmit",
24 | "postinstall": "npx simple-git-hooks",
25 | "prepare": "pnpm -r --filter=\"./packages/*\" run build"
26 | },
27 | "devDependencies": {
28 | "@antfu/eslint-config": "^0.43.1",
29 | "@storybook-vue/nuxt": "workspace:*",
30 | "@types/node": "^20.11.30",
31 | "@typescript-eslint/eslint-plugin": "^5.62.0",
32 | "@typescript-eslint/parser": "^5.62.0",
33 | "eslint": "^8.57.0",
34 | "eslint-config-airbnb-typescript": "^18.0.0",
35 | "lint-staged": "^15.2.2",
36 | "rimraf": "^5.0.5",
37 | "simple-git-hooks": "^2.11.1",
38 | "typescript": "^5.1.6",
39 | "ufo": "^1.5.3",
40 | "unbuild": "^2.0.0",
41 | "vite": "^5.0.0",
42 | "vite-hot-client": "^0.2.3",
43 | "vue-tsc": "^2.0.7"
44 | },
45 | "simple-git-hooks": {
46 | "pre-commit": "pnpm lint-staged"
47 | },
48 | "lint-staged": {
49 | "*": "eslint --fix"
50 | },
51 | "publishConfig": {
52 | "access": "public"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .nuxt
4 | .nitro
5 | .cache
6 | dist
7 |
8 | # Editor files
9 | .vscode
10 |
11 | # Node dependencies
12 | node_modules
13 |
14 | # Logs
15 | logs
16 | *.log
17 |
18 | # Misc
19 | .DS_Store
20 | .fleet
21 | .idea
22 |
23 | # Local env files
24 | .env
25 | .env.*
26 | !.env.example
27 | # If you need to ignore a file that is not in .gitignore
28 | packages/cli
29 | packages/storybook-nuxt-cli/test-pnpm/package.json
30 | packages/storybook-nuxt-cli/test-pnpm/.storybook/main.ts
31 | packages/storybook-nuxt-cli/test-pnpm/.storybook/preview.ts
32 | packages/storybook-nuxt-cli/test-pnpm/stories/button.css
33 | packages/storybook-nuxt-cli/test-pnpm/stories/Configure.mdx
34 | packages/storybook-nuxt-cli/test-pnpm/stories/header.css
35 | packages/storybook-nuxt-cli/test-pnpm/stories/MyNuxtWelcome.stories.ts
36 | packages/storybook-nuxt-cli/test-pnpm/stories/MyWelcome.vue
37 | packages/storybook-nuxt-cli/test-pnpm/stories/page.css
38 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/accessibility.svg
39 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/assets.jpg
40 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/checkmark.svg
41 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/chromatic.svg
42 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/context.jpg
43 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/discord.svg
44 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/document.svg
45 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/figma.svg
46 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/github.svg
47 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/styling.jpg
48 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/tutorials.svg
49 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/typography.svg
50 | packages/storybook-nuxt-cli/test-pnpm/stories/assets/youtube.svg
51 | packages/storybook-nuxt-cli/test-pnpm/package.json
52 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/.storybook/rendererAssets/common/assets/chromatic.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/src/runtime/plugins/storybook.ts:
--------------------------------------------------------------------------------
1 | import { createNuxtApp, defineNuxtPlugin } from 'nuxt/app'
2 | import { getContext } from 'unctx'
3 |
4 | // import logger from 'consola'
5 |
6 | import type { App } from 'vue'
7 | import { createRouter, createWebHistory } from 'vue-router'
8 |
9 | // @ts-expect-error virtual file
10 | import plugins from '#build/plugins'
11 |
12 | import '#build/css'
13 |
14 | const globalWindow = window as any
15 | const logger = console
16 |
17 | export default defineNuxtPlugin({
18 | name: 'storybook-nuxt-plugin',
19 | enforce: 'pre', // or 'post'
20 |
21 | setup(nuxtApp: any) {
22 | logger.log('🔌 🔌 🔌 [storybook-nuxt-plugin] setup ', { nuxtApp })
23 | const nuxtMainApp = getContext('nuxt-app')
24 | if (nuxtMainApp)
25 | logger.info('🔌 [storybook-nuxt-plugin] setup already done ', nuxtMainApp)
26 |
27 | if (nuxtApp.globalName !== 'nuxt')
28 | return
29 | const applyNuxtPlugins = async (vueApp: App, storyContext: any) => {
30 | const nuxt = createNuxtApp({ vueApp, globalName: `nuxt-${storyContext.id}` })
31 | getContext('nuxt-app').set(nuxt, true)
32 |
33 | const router = nuxtApp.$router ?? createRouter({ history: createWebHistory(), routes: [] })
34 | nuxt.$router = router
35 |
36 | getContext(nuxt.globalName).set(nuxt, true)
37 |
38 | nuxt.hooks.callHook('app:created', vueApp)
39 | for (const plugin of plugins) {
40 | try {
41 | if (typeof plugin === 'function' && !plugin.toString().includes('definePayloadReviver'))
42 | await vueApp.runWithContext(() => plugin(nuxt))
43 | }
44 | catch (e) {
45 | logger.error('Error in plugin ', plugin)
46 | }
47 | }
48 |
49 | return nuxt
50 | }
51 |
52 | globalWindow.PLUGINS_SETUP_FUNCTIONS ||= new Set()
53 | globalWindow.PLUGINS_SETUP_FUNCTIONS.add(applyNuxtPlugins)
54 | },
55 |
56 | hooks: {
57 | 'app:created': function () {
58 | },
59 | },
60 | })
61 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: release
5 |
6 | on:
7 | release:
8 | types: [created]
9 | push:
10 | tags:
11 | - 'release*'
12 | - 'v*'
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v3
19 | - uses: actions/setup-node@v3
20 | with:
21 | node-version: 16
22 | - uses: pnpm/action-setup@v2
23 | name: Install pnpm
24 | with:
25 | version: 7
26 | run_install: false
27 | - name: Get pnpm store directory
28 | shell: bash
29 | run: |
30 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
31 |
32 | - uses: actions/cache@v3
33 | name: Setup pnpm cache
34 | with:
35 | path: ${{ env.STORE_PATH }}
36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
37 | restore-keys: |
38 | ${{ runner.os }}-pnpm-store-
39 |
40 | - name: Install dependencies
41 | run: pnpm install --no-frozen-lockfile
42 |
43 | publish-npm:
44 | runs-on: ubuntu-latest
45 | needs: build
46 | environment: development
47 | steps:
48 | - name: Checkout 🛎️
49 | uses: actions/checkout@v3
50 |
51 | - uses: pnpm/action-setup@v2
52 |
53 | - name: Use Node LTS ✨
54 | uses: actions/setup-node@v3
55 | with:
56 | node-version: lts/*
57 | registry-url: https://registry.npmjs.org
58 | cache: pnpm
59 |
60 | - name: Install dependencies 📦️
61 | run: pnpm install --no-frozen-lockfile
62 |
63 | - name: Build 🔨
64 | run: pnpm build
65 |
66 | - name: Publish 🚀️
67 | run: pnpm publish --recursive --access public --report-summary
68 | env:
69 | NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}}
70 |
--------------------------------------------------------------------------------
/.github/workflows/release-next.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: release
5 |
6 | on:
7 | release:
8 | types: [created]
9 | push:
10 | branches:
11 | - next
12 | tags:
13 | - 'dev*'
14 | - 'test*'
15 | - 'beta*'
16 | - 'alpha*'
17 |
18 | jobs:
19 | build:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v3
23 | - uses: actions/setup-node@v3
24 | with:
25 | node-version: 16
26 | - uses: pnpm/action-setup@v2
27 | name: Install pnpm
28 | with:
29 | version: 7
30 | run_install: false
31 | - name: Get pnpm store directory
32 | shell: bash
33 | run: |
34 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
35 |
36 | - uses: actions/cache@v3
37 | name: Setup pnpm cache
38 | with:
39 | path: ${{ env.STORE_PATH }}
40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
41 | restore-keys: |
42 | ${{ runner.os }}-pnpm-store-
43 |
44 | - name: Install dependencies
45 | run: pnpm install --no-frozen-lockfile
46 |
47 | publish-npm:
48 | runs-on: ubuntu-latest
49 | needs: build
50 | environment: development
51 | steps:
52 | - name: Checkout 🛎️
53 | uses: actions/checkout@v3
54 |
55 | - uses: pnpm/action-setup@v2
56 |
57 | - name: Use Node LTS ✨
58 | uses: actions/setup-node@v3
59 | with:
60 | node-version: lts/*
61 | registry-url: https://registry.npmjs.org
62 | cache: pnpm
63 |
64 | - name: Install dependencies 📦️
65 | run: pnpm install --no-frozen-lockfile
66 |
67 | - name: Build 🔨
68 | run: pnpm build
69 |
70 | - name: Publish 🚀️
71 | run: pnpm publish packages/storybook-nuxt --tag next --access public --report-summary
72 | env:
73 | NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}}
74 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/.storybook/rendererAssets/common/assets/tutorials.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/playground/components/pinia/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | {{ msg }}
18 |
19 |
20 |
21 |
22 | This is an example store to test out devtools. Try one of the following
23 | with the devtools open:
24 |
25 |
26 |
27 |
28 | - Use the different increment actions
29 | - Change the counter directly from the devtools
30 | - Use decrement to zero to see how action groups work
31 | -
32 | Click
33 | Test Errors and immediately after increment the store
34 |
35 | -
36 | While the dev server is running, try changing counter.changeMe, adding,
37 | and removing new state properties
38 |
39 |
40 |
41 | Counter Store
42 |
43 |
44 | Counter: {{ counter.n }}. Double: {{ counter.double }}
45 |
46 |
47 | Increment the Store:
48 |
49 |
50 | +1
51 |
52 |
53 | +10
54 |
55 |
56 | +100
57 |
58 |
59 | Direct Increment
60 |
61 | {
64 | state.n++
65 | state.incrementedTimes++
66 | })
67 | "
68 | >
69 | Direct patch
70 |
71 |
72 | Other actions:
73 |
74 |
75 | Test Errors
76 |
77 |
78 | Decrement to zero
79 |
80 |
81 | counter.changeMe()
82 |
83 |
84 |
85 |
86 | {{ counter.$state }}
87 |
88 |
89 |
90 |
101 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/README.md:
--------------------------------------------------------------------------------
1 | # Storybook for Nuxt framework
2 |
3 |
4 | 
5 |
6 |
7 | Storybook package for [**Nuxt framework**](https://nuxt.com/) with zero configs. seamless integration supporting all Nuxt fancy features
8 |
9 |
10 |
11 | https://github.com/storybook-vue/nuxt/assets/711292/e66a1899-ab7c-42dd-b358-59e22ff0f609
12 |
13 |
14 |
15 | ## Supported Features
16 |
17 | 👉 [Nuxt Modules](#nuxts-image-component)
18 |
19 | 👉 [Nuxt Plugins](#nuxt-font-optimization)
20 |
21 | 👉 [All in-built Nuxt Components](#nuxt-components)
22 |
23 | 👉 [Sass/Scss](#sassscss)
24 |
25 | 👉 [Css/Sass/Scss Modules](#csssassscss-modules)
26 |
27 | 👉 [ JSX ](#styled-jsx)
28 |
29 | 👉 [Postcss](#postcss)
30 |
31 | 👉 [Auto Imports](#auto-imports)
32 |
33 | 👉 [Runtime Config](#runtime-config)
34 |
35 | 👉 [Composables](#composables)
36 |
37 | 👉 [Typescript](#typescript) (already supported out of the box by Storybook)
38 |
39 | 👉 [Nuxt DevTools](https://devtools.nuxtjs.org/) : finally as Bonus, Nuxt DevTools works amazingly with your Storybook, full features
40 |
41 |
42 | https://github.com/storybook-vue/nuxt/assets/711292/63cc1fb3-ec6b-4df2-ad61-d87e5692f385
43 |
44 |
45 |
46 | ## Requirements
47 |
48 | - [Nuxt](https://nuxt.com/) >= 3.x
49 | - [Storybook](https://storybook.js.org/) >= 7.x
50 |
51 | ## Demo
52 |
53 | Checkout the demo repo [storybook7-nuxt3-demo](https://github.com/storybook-vue/storybook-nuxt-demo)
54 | or try it on [Stackblitz](https://stackblitz.com/~/github.com/storybook-vue/storybook-nuxt-demo)
55 |
56 | ## Getting Started
57 |
58 | ### In a project without Storybook
59 |
60 | Follow the prompts after running this command in your Nuxt project's root directory:
61 |
62 | ```bash
63 | npx storybook-nuxt init
64 | ```
65 |
66 | [More on getting started with Storybook](https://storybook.js.org/docs/vue3/get-started/install)
67 |
68 | #### Automatic migration
69 |
70 | When running the `upgrade` command above, you should get a prompt asking you to migrate to `@storybook-vue/nuxt`, which should handle everything for you. In case auto-migration does not work for your project, refer to the manual migration below.
71 |
72 |
73 |
74 | Update your `main.js` to change the framework property:
75 |
76 | ```js
77 | // .storybook/main.js
78 | export default {
79 | // ...
80 | framework: {
81 | name: '@storybook-vue/nuxt', // Add this
82 | options: {},
83 | },
84 | }
85 | ```
86 |
87 | ## Documentation
88 |
89 | In progress
90 |
91 | ## License
92 |
93 | This repository is licensed under the [MIT License](LICENSE). Feel free to use the code and modify it according to your needs.
94 |
95 | ## Contacts :
96 |
97 | 🔖 Mail: javachakir@gmail.com
98 |
99 | 💬 Discord: ChakAs3
100 |
101 | 🐦⬛ Twitter: [@ChakirQatab](https://twitter.com/ChakirQatab)
102 |
103 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@storybook-vue/nuxt",
3 | "type": "module",
4 | "version": "0.2.7",
5 | "packageManager": "pnpm@8.6.12",
6 | "description": "Storybook for Nuxt and Vite: Develop Vue3 components in isolation with Hot Reloading.",
7 | "license": "MIT",
8 | "homepage": "https://github.com/storybook-vue/storybook-nuxt",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/storybook-vue/storybook-nuxt.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/storybook-vue/storybook-nuxt/issues"
15 | },
16 | "keywords": [
17 | "storybook",
18 | "nuxt",
19 | "vite",
20 | "vue3"
21 | ],
22 | "exports": {
23 | ".": {
24 | "types": "./dist/index.d.ts",
25 | "node": "./dist/index.cjs",
26 | "import": "./dist/index.mjs",
27 | "require": "./dist/index.cjs"
28 | },
29 | "./preset": {
30 | "types": "./dist/preset.d.ts",
31 | "require": "./dist/preset.cjs"
32 | },
33 | "./preview": {
34 | "types": "./dist/preview.d.ts",
35 | "import": "./dist/preview.mjs",
36 | "default": "./preview.js"
37 | },
38 | "./package.json": "./package.json"
39 | },
40 | "main": "dist/index.cjs",
41 | "module": "dist/index.mjs",
42 | "types": "dist/index.d.ts",
43 | "files": [
44 | "dist/**/*",
45 | "template/**/*",
46 | "README.md",
47 | "*.js",
48 | "*.mjs",
49 | "*.cjs",
50 | "*.d.ts"
51 | ],
52 | "engines": {
53 | "node": ">=18.0.0"
54 | },
55 | "scripts": {
56 | "build": "unbuild",
57 | "build:watch": "unbuild --stub",
58 | "test": "vitest run",
59 | "dev": "unbuild && cd playground && storybook dev -p 6006",
60 | "prepack": "unbuild",
61 | "release": "pnpm changelogen --release --push && pnpm publish"
62 | },
63 | "peerDependencies": {
64 | "nuxt": "^3.6 || ^3.7 || ^3.8",
65 | "vite": "^3.0.0 || ^4.0.0 || ^5.0.0",
66 | "vue": "^3.0.0"
67 | },
68 | "dependencies": {
69 | "@nuxt/devtools-kit": "^1.0.6",
70 | "@nuxt/kit": "^3.11.1",
71 | "@nuxt/schema": "^3.11.1",
72 | "@nuxt/types": "2.17.2",
73 | "@nuxt/vite-builder": "^3.11.1",
74 | "@storybook/builder-vite": "^8.0.0",
75 | "@storybook/vue3": "^8.0.0",
76 | "@storybook/vue3-vite": "^8.0.0",
77 | "autoprefixer": "^10.4.16",
78 | "nuxt": "^3.11.1",
79 | "postcss": "^8.4.33",
80 | "postcss-import": "^15.1.0",
81 | "postcss-url": "^10.1.3",
82 | "typescript": "^5.4.3",
83 | "vue": "^3.4.21"
84 | },
85 | "devDependencies": {
86 | "@storybook/types": "^8.0.0",
87 | "@vitejs/plugin-vue": "^5.0.4",
88 | "@vitejs/plugin-vue-jsx": "^3.1.0",
89 | "changelogen": "^0.5.5",
90 | "unbuild": "^2.0.0",
91 | "vite": "^5.2.2"
92 | },
93 | "publishConfig": {
94 | "access": "public"
95 | },
96 | "bundler": {
97 | "entries": [
98 | "./src/index.ts",
99 | "./src/preview.ts",
100 | "./src/preset.ts"
101 | ],
102 | "platform": "node"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Storybook for Nuxt framework
2 |
3 |
4 | 
5 |
6 |
7 | Storybook package for [**Nuxt framework**](https://nuxt.com/) with zero configs. seamless integration supporting all Nuxt fancy features
8 |
9 |
10 | https://www.veed.io/embed/1f9d58b6-3c76-43a1-a15e-ae95af8dff5f
11 |
12 |
13 | ## Demos
14 |
15 | - Simple Demo [storybook7-nuxt3-demo](https://github.com/storybook-vue/storybook-nuxt-demo)
16 | or try it on [Stackblitz](https://stackblitz.com/~/github.com/storybook-vue/storybook-nuxt-demo)
17 |
18 |
19 | - Demo with more plugins [i18n+pinia](https://github.com/chakAs3/nuxt-sb-app) try it on [Stackblitz](https://stackblitz.com/~/github.com/chakAs3/nuxt-sb-app)
20 | - Full Demo [i18n, Pinia, Composables,Vuetify,and Routing](https://github.com/storybook-vue/storybook-nuxt-starter) try it on [Stackblitz](https://stackblitz.com/~/github.com/storybook-vue/storybook-nuxt-starter)
21 |
22 | ## Storybook module for setup:
23 |
24 | [Storybook Nuxt-Module](https://github.com/storybook-vue/nuxt-storybook)
25 |
26 | [Storybook Nuxt-Module Demo](https://github.com/chakAs3/nuxt-storybook-module-demo)
27 |
28 | ## Supported Features
29 |
30 | 👉 [Nuxt Modules](#nuxts-image-component)
31 |
32 | 👉 [Nuxt Plugins](#nuxt-font-optimization)
33 |
34 | 👉 [All in-built Nuxt Components](#nuxt-components)
35 |
36 | 👉 [Sass/Scss](#sassscss)
37 |
38 | 👉 [Css/Sass/Scss Modules](#csssassscss-modules)
39 |
40 | 👉 [ JSX ](#styled-jsx)
41 |
42 | 👉 [Postcss](#postcss)
43 |
44 | 👉 [Auto Imports](#auto-imports)
45 |
46 | 👉 [Runtime Config](#runtime-config)
47 |
48 | 👉 [Composables](#composables)
49 |
50 | 👉 [Typescript](#typescript) (already supported out of the box by Storybook)
51 |
52 | 👉 [Nuxt DevTools](https://devtools.nuxtjs.org/) : finally as Bonus, Nuxt DevTools works amazingly with your Storybook, full features
53 |
54 |
55 | https://github.com/storybook-vue/nuxt/assets/711292/63cc1fb3-ec6b-4df2-ad61-d87e5692f385
56 |
57 |
58 |
59 | ## Requirements
60 |
61 | - [Nuxt](https://nuxt.com/) >= 3.x
62 | - [Storybook](https://storybook.js.org/) >= 7.x
63 |
64 |
65 | ## Getting Started
66 |
67 | ### In a project without Storybook
68 |
69 | Follow the prompts after running this command in your Nuxt project's root directory:
70 |
71 | ```bash
72 | npx storybook-nuxt init
73 | ```
74 |
75 | [More on getting started with Storybook](https://storybook.js.org/docs/vue3/get-started/install)
76 |
77 | #### Automatic migration
78 |
79 | When running the `upgrade` command above, you should get a prompt asking you to migrate to `@storybook-vue/nuxt`, which should handle everything for you. In case auto-migration does not work for your project, refer to the manual migration below.
80 |
81 |
82 |
83 | Update your `main.js` to change the framework property:
84 |
85 | ```js
86 | // .storybook/main.js
87 | export default {
88 | // ...
89 | framework: {
90 | name: '@storybook-vue/nuxt', // Add this
91 | options: {},
92 | },
93 | }
94 | ```
95 |
96 | ## Documentation
97 |
98 | check https://storybook.nuxtjs.org/
99 |
100 | ## License
101 |
102 | This repository is licensed under the [MIT License](LICENSE). Feel free to use the code and modify it according to your needs.
103 |
104 | ## Contacts :
105 |
106 | 🔖 Mail: javachakir@gmail.com
107 |
108 | 💬 Discord: ChakAs3
109 |
110 | 🐦⬛ Twitter: [@ChakirQatab](https://twitter.com/ChakirQatab)
111 |
112 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/src/add-module.ts:
--------------------------------------------------------------------------------
1 | import { existsSync } from 'node:fs'
2 | import fsp from 'node:fs/promises'
3 | import { relative } from 'node:path'
4 | import { cwd } from 'node:process'
5 |
6 | // import { consola } from 'consola'
7 | import c from 'picocolors'
8 | import { parseModule } from 'magicast'
9 | import { diffLines } from 'diff'
10 | import path, { join } from 'pathe'
11 |
12 | const consola = console
13 |
14 | export async function addModuleToNuxtConfigFile(moduleName, cwd) {
15 | const nuxtConfig = findNuxtConfig(cwd)
16 | if (!nuxtConfig) {
17 | consola.error(c.red('Unable to find Nuxt config file in current directory:'), cwd)
18 | process.exitCode = 1
19 | printOutManual(moduleName)
20 | return false
21 | }
22 |
23 | try {
24 | const source = await fsp.readFile(nuxtConfig, 'utf-8')
25 | const mod = await parseModule(source, { sourceFileName: nuxtConfig })
26 | const config = mod.exports.default.$type === 'function-call'
27 | ? mod.exports.default.$args[0]
28 | : mod.exports.default
29 |
30 | config.modules ||= []
31 | if (typeof config.modules === 'object' && !config.modules.includes(moduleName))
32 | config.modules.push(moduleName)
33 |
34 | const generated = mod.generate().code
35 |
36 | if (source.trim() === generated.trim()) {
37 | consola.info(c.yellow('x'))
38 | }
39 | else {
40 | consola.log('')
41 | consola.log('We are going to update the Nuxt config with with the following changes:')
42 | consola.log(c.bold(c.green(`./${relative(cwd, nuxtConfig)}`)))
43 | consola.log('')
44 | printDiffToCLI(source, generated)
45 | consola.log('')
46 |
47 | await fsp.writeFile(nuxtConfig, `${generated.trimEnd()}\n`, 'utf-8')
48 | }
49 | }
50 | catch (err) {
51 | consola.error(c.red('Unable to update Nuxt config file automatically'))
52 | process.exitCode = 1
53 | printOutManual(moduleName)
54 | return false
55 | }
56 | }
57 |
58 | function findNuxtConfig(cwd) {
59 | const names = [
60 | 'nuxt.config.ts',
61 | 'nuxt.config.js',
62 | ]
63 |
64 | for (const name of names) {
65 | const path = join(cwd, name)
66 | if (existsSync(path))
67 | return path
68 | }
69 | }
70 | function printOutManual(moduleName: boolean) {
71 | consola.info(c.yellow('To manually enable Storybook Module, add the following to your Nuxt config modules :'))
72 | consola.info(c.cyan(`\n ${moduleName} \n`))
73 | }
74 |
75 | // diff `from` and `to` by line and pretty print to console with line numbers, using the `diff` package
76 | function printDiffToCLI(from, to) {
77 | const diffs = diffLines(from.trim(), to.trim())
78 | let output = ''
79 |
80 | let no = 0
81 |
82 | // TODO: frame only the diff parts
83 | for (const diff of diffs) {
84 | const lines = diff.value.trimEnd().split('\n')
85 | for (const line of lines) {
86 | if (!diff.added)
87 | no += 1
88 | if (diff.added)
89 | output += c.green(`+ | ${line}\n`)
90 | else if (diff.removed)
91 | output += c.red(`-${no.toString().padStart(3, ' ')} | ${line}\n`)
92 | else
93 | output += c.gray(`${c.dim(`${no.toString().padStart(4, ' ')} |`)} ${line}\n`)
94 | }
95 | }
96 |
97 | consola.log(output.trimEnd())
98 | }
99 |
100 | export async function updatePackageJsonFile(devDependencies) {
101 | try {
102 | const packageJsonPath = path.join(process.cwd(), 'package.json')
103 | const source = await fsp.readFile(packageJsonPath, 'utf-8')
104 |
105 | const packageJson = source ? JSON.parse(source) : {}
106 |
107 | packageJson.devDependencies ||= {}
108 | if (typeof packageJson.devDependencies === 'object') {
109 | for (const [name, version] of Object.entries(devDependencies))
110 | packageJson.devDependencies[name] = version
111 | }
112 |
113 | const generated = JSON.stringify(packageJson, null, 2)
114 |
115 | if (source.trim() === generated.trim()) {
116 | consola.info(c.yellow('x'))
117 | }
118 | else {
119 | consola.log('')
120 | consola.log(`We are going to update ${c.blue('package.json')} with the following changes:`)
121 | consola.log(c.bold(c.green(`./${relative(cwd(), packageJsonPath)}`)))
122 | consola.log('')
123 | printDiffToCLI(source, generated)
124 | consola.log('')
125 |
126 | await fsp.writeFile(packageJsonPath, `${generated.trimEnd()}\n`, 'utf-8')
127 | }
128 | }
129 | catch (err) {
130 | consola.error(c.red('Unable to update package.json file automatically'), err)
131 | process.exitCode = 1
132 |
133 | return false
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/.storybook/rendererAssets/common/assets/typography.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/src/preset.ts:
--------------------------------------------------------------------------------
1 | import { dirname, join, resolve } from 'node:path'
2 | import { fileURLToPath, pathToFileURL } from 'node:url'
3 | import { createRequire } from 'node:module'
4 |
5 | import type { PresetProperty } from '@storybook/types'
6 | import { type UserConfig as ViteConfig, mergeConfig, searchForWorkspaceRoot } from 'vite'
7 | import type { Nuxt } from '@nuxt/schema'
8 | import vuePlugin from '@vitejs/plugin-vue'
9 |
10 | import replace from '@rollup/plugin-replace'
11 | import type { StorybookConfig } from './types'
12 | import { componentsDir, composablesDir, pluginsDir, runtimeDir } from './dirs'
13 |
14 | const packageDir = resolve(fileURLToPath(
15 | import.meta.url), '../..')
16 | const distDir = resolve(fileURLToPath(
17 | import.meta.url), '../..', 'dist')
18 |
19 | const dirs = [distDir, packageDir, pluginsDir, componentsDir]
20 |
21 | let nuxt: Nuxt
22 |
23 | /**
24 | * extend nuxt-link component to use storybook router
25 | * @param nuxt
26 | */
27 | function extendComponents(nuxt: Nuxt) {
28 | nuxt.hook('components:extend', (components: any) => {
29 | const nuxtLink = components.find(({ name }: any) => name === 'NuxtLink')
30 | nuxtLink.filePath = join(runtimeDir, 'components/nuxt-link')
31 | nuxtLink.shortPath = join(runtimeDir, 'components/nuxt-link')
32 | nuxt.options.build.transpile.push(nuxtLink.filePath)
33 | })
34 | }
35 |
36 | /**
37 | * extend composables to override router ( fix undefined router useNuxtApp )
38 | *
39 | * @param nuxt
40 | */
41 |
42 | async function extendComposables(nuxt: Nuxt) {
43 | const { addImportsSources } = await import('@nuxt/kit')
44 | nuxt.options.build.transpile.push(composablesDir)
45 | addImportsSources({ imports: ['useRouter'], from: join(composablesDir, 'router') })
46 | }
47 |
48 | async function defineNuxtConfig(baseConfig: Record) {
49 | const { loadNuxt, buildNuxt, addPlugin, extendPages } = await import('@nuxt/kit')
50 |
51 | nuxt = await loadNuxt({
52 | rootDir: baseConfig.root,
53 | ready: false,
54 | dev: false,
55 |
56 |
57 | overrides: {
58 | buildDir: '.nuxt-storybook',
59 | },
60 |
61 | })
62 |
63 | if ((nuxt.options.builder as string) !== '@nuxt/vite-builder')
64 | throw new Error(`Storybook-Nuxt does not support '${nuxt.options.builder}' for now.`)
65 |
66 | let extendedConfig: ViteConfig = {}
67 | nuxt.options.build.transpile.push(join(packageDir, 'preview'))
68 |
69 | nuxt.hook('modules:done', () => {
70 | extendComposables(nuxt)
71 | // Override nuxt-link component to use storybook router
72 | extendComponents(nuxt)
73 | // nuxt.options.build.transpile.push('@storybook-vue/nuxt')
74 | addPlugin({
75 | src: join(pluginsDir, 'storybook'),
76 | mode: 'client',
77 | })
78 | // Add iframe page
79 | extendPages((pages: any) => {
80 | pages.push({
81 | name: 'storybook-iframe',
82 | path: '/iframe.html',
83 | })
84 | })
85 |
86 | nuxt.hook(
87 | 'vite:extendConfig',
88 | (
89 | config: ViteConfig | PromiseLike | Record,
90 | { isClient }: any,
91 | ) => {
92 | if (isClient) {
93 | const plugins = baseConfig.plugins
94 |
95 | // Find the index of the plugin with name 'vite:vue'
96 | const index = plugins.findIndex((plugin: any) => plugin.name === 'vite:vue')
97 |
98 | // Check if the plugin was found
99 | if (index !== -1) {
100 | // Replace the plugin with the new one using vuePlugin()
101 | plugins[index] = vuePlugin()
102 | }
103 | else {
104 | plugins.push(vuePlugin())
105 | }
106 | baseConfig.plugins = plugins
107 | extendedConfig = mergeConfig(config, baseConfig)
108 | }
109 | },
110 | )
111 | })
112 |
113 | await nuxt.ready()
114 |
115 | try {
116 | await buildNuxt(nuxt)
117 |
118 | return {
119 | viteConfig: extendedConfig,
120 | nuxt,
121 | }
122 | }
123 | catch (e: any) {
124 | throw new Error(e)
125 | }
126 | }
127 | export const core: PresetProperty<'core', StorybookConfig> = async (config: any) => {
128 | return ({
129 | ...config,
130 | builder: '@storybook/builder-vite',
131 | renderer: '@storybook/vue3',
132 | })
133 | }
134 | /**
135 | *
136 | * @param entry preview entries
137 | * @returns preview entries with nuxt runtime
138 | */
139 | export const previewAnnotations: StorybookConfig['previewAnnotations'] = async (entry = []) => {
140 | return [...entry, resolve(packageDir, 'preview')]
141 | }
142 |
143 | export const viteFinal: StorybookConfig['viteFinal'] = async (
144 | config: Record,
145 | options: any,
146 | ) => {
147 | const getStorybookViteConfig = async (c: Record, o: any) => {
148 | // const pkgPath = await getPackageDir('@storybook/vue3-vite')
149 | const presetURL = pathToFileURL(join(await getPackageDir('@storybook/vue3-vite'), 'preset.js'))
150 | const { viteFinal: ViteFile } = await import(presetURL.href)
151 |
152 | if (!ViteFile)
153 | throw new Error('ViteFile not found')
154 | return ViteFile(c, o)
155 | }
156 | const nuxtConfig = await defineNuxtConfig(await getStorybookViteConfig(config, options))
157 |
158 | return mergeConfig(nuxtConfig.viteConfig, {
159 | // build: { rollupOptions: { external: ['vue', 'vue-demi'] } },
160 | define: {
161 | '__NUXT__': JSON.stringify({ config: nuxtConfig.nuxt.options.runtimeConfig }),
162 | 'import.meta.client': 'true',
163 | },
164 |
165 | plugins: [replace({
166 | values: {
167 | 'import.meta.server': 'false',
168 | 'import.meta.client': 'true',
169 | },
170 | preventAssignment: true,
171 | })],
172 | server: {
173 | cors: true,
174 | proxy: {
175 | ...getPreviewProxy(),
176 | ...getNuxtProxyConfig(nuxt).proxy,
177 | },
178 | fs: { allow: [searchForWorkspaceRoot(process.cwd()), ...dirs] },
179 | },
180 | envPrefix: ['NUXT_'],
181 | })
182 | }
183 |
184 | async function getPackageDir(frameworkPackageName: any) {
185 | // const packageJsonPath = join(frameworkPackageName, 'package.json')
186 |
187 | try {
188 | const require = createRequire(import.meta.url)
189 | const packageDir = dirname(require.resolve(join(frameworkPackageName, 'package.json'), { paths: [process.cwd()] }))
190 |
191 | return packageDir
192 | }
193 | catch (e) {
194 | // logger.error(e)
195 | }
196 | throw new Error(`Cannot find ${frameworkPackageName},`)
197 | }
198 |
199 | export function getNuxtProxyConfig(nuxt: Nuxt) {
200 | const port = nuxt.options.runtimeConfig.app.port ?? 3000
201 | const route = '^/(_nuxt|_ipx|_icon|__nuxt_devtools__)'
202 | const proxy = {
203 | [route]:
204 | {
205 | target: `http://localhost:${port}`,
206 | changeOrigin: true,
207 | secure: false,
208 | ws: true,
209 | },
210 | }
211 | return {
212 | port,
213 | route,
214 | proxy,
215 | }
216 | }
217 |
218 | function getPreviewProxy() {
219 | return {
220 | '/__storybook_preview__': {
221 | target: '/',
222 | changeOrigin: false,
223 | secure: false,
224 | rewrite: (path: string) => path.replace('/__storybook_preview__', ''),
225 | ws: true,
226 | },
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/.storybook/rendererAssets/common/Configure.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/blocks";
2 |
3 | import Accessibility from "./assets/accessibility.svg";
4 | import Checkmark from "./assets/checkmark.svg";
5 | import Document from "./assets/document.svg";
6 | import Typography from "./assets/typography.svg";
7 | import Github from "./assets/github.svg";
8 | import Discord from "./assets/discord.svg";
9 | import Youtube from "./assets/youtube.svg";
10 | import Chromatic from "./assets/chromatic.svg";
11 | import Figma from "./assets/figma.svg";
12 | import Tutorials from "./assets/tutorials.svg";
13 | import Styling from "./assets/styling.jpg";
14 | import Context from "./assets/context.jpg";
15 | import Assets from "./assets/assets.jpg";
16 |
17 |
18 |
19 | # Configure your project
20 |
21 | Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
22 |
23 |
24 |
25 |
29 |
30 | Add styling and CSS
31 | Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.
32 |
36 | Read more on how to set up styling
37 | ›
38 |
39 |
40 |
41 |
42 |
46 |
47 | Provide context and mocking
48 | Often when a story doesn't render, it's because your component is
49 | expecting a specific environment or context (like a theme provider) to be available.
50 | Learn more about solving these issues by providing context and mocking to Storybook.
51 | Read more on how to set up context ›
55 |
56 |
57 |
58 |
59 |
60 | Load assets and resources
61 | To link static files (like fonts) to your projects and stories, use the `staticDirs` configuration option to specify folders to load when starting Storybook.
62 | Read more on how to load assets ›
66 |
67 |
68 |
69 |
70 | # Do more with Storybook
71 |
72 | Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | Autodocs
81 | Auto-generate living, interactive reference documentation from your components and stories.
82 | Learn more ›
86 |
87 |
88 |
89 | Testing
90 | Use stories to test a component in all its variations, no matter how complex.
91 | Learn more ›
95 |
96 |
97 |
98 | Publish to Chromatic
99 | Publish your Storybook to review and collaborate with your entire team.
100 | Learn more ›
104 |
105 |
106 |
107 | Figma Plugin
108 | Embed your stories into Figma to cross-reference the design and live implementation in one place.
109 | Learn more ›
113 |
114 |
115 |
116 | Accessibility
117 | Automatically test your components for a11y issues as you develop.
118 | Learn more ›
122 |
123 |
124 |
125 | Theming
126 | Theme Storybook's UI to personalize it to your project.
127 | Learn more ›
131 |
132 |
133 |
134 |
135 | # Explore and Connect
136 |
137 | Connect with our community on Discord or start contributing directly on Github. You might also be interesting in watching some videos on Youtube explaining how to take full advantage of Storybook.
138 |
139 |
140 |
141 |
142 |
143 |
148 |
149 | Discord
150 |
151 |
156 |
157 | Github
158 |
159 |
164 |
165 | Youtube
166 |
167 |
172 |
173 | Tutorials
174 |
175 |
176 |
177 |
178 |
287 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt-cli/src/init.ts:
--------------------------------------------------------------------------------
1 | import { spawn } from 'node:child_process'
2 | import fsp from 'node:fs/promises'
3 | import path from 'node:path'
4 | import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
5 | import { createRequire } from 'node:module'
6 | import { fileURLToPath } from 'node:url'
7 | import c from 'picocolors'
8 | import { consola } from 'consola'
9 | import { addModuleToNuxtConfigFile, updatePackageJsonFile } from './add-module'
10 |
11 | // Unicode icons for better display
12 | const CHECKMARK = '\u2714' // ✔
13 | const CROSSMARK = '\u274C' // ❌
14 | const STARTMARK = '\u25B6' // ▶
15 |
16 | const sbVersion = '8.0.8'
17 | const nuxtSbVersion = '0.2.6'
18 | const nuxtSbModuleVersion = '7.0.2'
19 |
20 | const logger = console
21 | let packageManager
22 |
23 | async function initStorybook(start = false, port = 6006, ci = true, enableModule = false) {
24 | logger.log(`${STARTMARK} Initializing Storybook configuration...`)
25 | logger.log()
26 | // Path to the project root
27 | const projectRoot = process.cwd()
28 | // Path to the root of the Storybook Nuxt CLI
29 | const rootDir = path.join(fileURLToPath(import.meta.url), '../..')
30 | // Path to the .storybook directory
31 | const storybookDir = path.join(projectRoot, '.storybook')
32 |
33 | // Determine if the project is using TypeScript
34 | const isTypeScriptProject = existsSync(path.join(projectRoot, 'tsconfig.json'))
35 |
36 | // Determine if the project has a ./src directory
37 | const hasSrcDirectory = existsSync(path.join(projectRoot, 'src'))
38 | const sourceFolder = hasSrcDirectory ? 'src' : '.'
39 |
40 | // Choose the appropriate file extension for the configuration files
41 | const configFileExtension = isTypeScriptProject ? 'ts' : 'js'
42 | const extensions = isTypeScriptProject ? 'js|jsx|mjs|ts|tsx' : 'js|jsx|mjs'
43 | // Build the paths for stories based on the source folder
44 | const storiesPath = path.join(sourceFolder, '../stories')
45 | const storiesGlob = `${storiesPath}/**/*.stories.@(${extensions})`
46 |
47 | // Load the main and preview template files
48 | const mainTemplatePath = path.join(rootDir, '.storybook', `main-${configFileExtension}`)
49 | const previewTemplatePath = path.join(rootDir, '.storybook', `preview-${configFileExtension}`)
50 |
51 | const mainTemplate = readFileSync(mainTemplatePath, 'utf8')
52 | const previewTemplate = readFileSync(previewTemplatePath, 'utf8')
53 |
54 | // Replace placeholders in the templates with dynamic values
55 | const mainConfigContent = mainTemplate
56 | .replace(/\$storiesPath/g, storiesPath)
57 | .replace(/\$storiesGlob/g, storiesGlob)
58 |
59 | const previewConfigContent = previewTemplate
60 |
61 | // Create the .storybook directory if it doesn't exist
62 | if (!existsSync(storybookDir))
63 | mkdirSync(storybookDir)
64 |
65 | // Create the Storybook main config file
66 | writeFileSync(path.join(storybookDir, `main.${configFileExtension}`), mainConfigContent)
67 |
68 | // Create the Storybook preview config file
69 | writeFileSync(path.join(storybookDir, `preview.${configFileExtension}`), previewConfigContent)
70 |
71 | logger.log('Install dependencies 📦️')
72 | logger.log()
73 |
74 | packageManager = detectPackageManager()
75 | if (!packageManager) {
76 | // Prompt user to select package manager
77 | const selectedPackageManager = await consola.prompt<{
78 | type: 'select'
79 | options: string[]
80 | }>('Which package manager would you like to use?', {
81 | type: 'select',
82 | options: ['npm', 'pnpm', 'yarn', 'bun'],
83 | })
84 |
85 | packageManager = selectedPackageManager
86 | }
87 |
88 | addDevDependencies()
89 | consola.info('🔌 enableModule ', enableModule)
90 | if (enableModule)
91 | addModuleToNuxtConfigFile('@nuxtjs/storybook', projectRoot)
92 | // Install required packages using pnpm
93 | const installProcess = spawn(packageManager, ['install'], {
94 | cwd: projectRoot,
95 | stdio: 'inherit',
96 | })
97 |
98 | installProcess.on('close', async (code) => {
99 | if (code !== 0) {
100 | logger.error(`${CROSSMARK} Package installation failed with code ${code}`)
101 | }
102 | else {
103 | logger.log(`${CHECKMARK} Packages installed successfully!`)
104 |
105 | await addScripts()
106 | await copyTemplateFiles(configFileExtension, path.join(sourceFolder, 'stories'))
107 |
108 | logger.log()
109 | logger.log('📕 Storybook is ready to go! 🚀')
110 | logger.log()
111 | logger.log('To start Storybook, run:')
112 | logger.log()
113 | logger.log(` ${c.blue(`${packageManager} run storybook`)} `)
114 | logger.log()
115 | if (start) {
116 | const startProcess = spawn(packageManager, ['storybook', 'dev', '--ci', '--port', `${port}`], {
117 | cwd: projectRoot,
118 | stdio: 'inherit',
119 | })
120 |
121 | startProcess.on('close', (code) => {
122 | if (code !== 0)
123 | logger.error(`${CROSSMARK} Storybook failed to start with code ${code}`)
124 | else
125 | logger.log(`${CHECKMARK} Storybook started successfully!`)
126 | })
127 | }
128 | }
129 | })
130 | }
131 |
132 | // Function to detect the package manager
133 | function detectPackageManager() {
134 | if (existsSync(path.join(process.cwd(), 'package-lock.json')))
135 | return 'npm'
136 |
137 | else if (existsSync(path.join(process.cwd(), 'yarn.lock')))
138 | return 'yarn'
139 |
140 | else if (existsSync(path.join(process.cwd(), 'pnpm-lock.yaml')))
141 | return 'pnpm'
142 |
143 | else if (existsSync(path.join(process.cwd(), 'bun.lock')))
144 | return 'bun'
145 |
146 | return undefined
147 | }
148 |
149 | async function addDevDependencies() {
150 | const devDependencies = {
151 | 'storybook': sbVersion,
152 | '@types/node': '^18.17.5',
153 | '@storybook/vue3': sbVersion,
154 | '@storybook-vue/nuxt': nuxtSbVersion,
155 | '@nuxtjs/storybook': nuxtSbModuleVersion,
156 | '@storybook/addon-links': sbVersion,
157 | '@storybook/builder-vite': sbVersion,
158 | '@storybook/addon-essentials': sbVersion,
159 | '@storybook/addon-interactions': sbVersion,
160 | '@storybook/test': sbVersion,
161 | '@storybook/blocks': sbVersion,
162 | }
163 |
164 | updatePackageJsonFile(devDependencies)
165 | }
166 |
167 | async function addScripts() {
168 | // Update package.json with the script
169 | const packageJsonPath = path.join(process.cwd(), 'package.json')
170 | const source = await fsp.readFile(packageJsonPath, 'utf-8')
171 | const packageJson = JSON.parse(source)
172 |
173 | if (packageJson) {
174 | packageJson.scripts = packageJson.scripts || {}
175 | packageJson.scripts.storybook = 'storybook dev --port 6006'
176 | packageJson.scripts['build-storybook'] = 'storybook build'
177 | writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
178 | logger.log()
179 | logger.log(`${CHECKMARK} Storybook scripts added to package.json `)
180 | }
181 | else {
182 | logger.log(`${CROSSMARK} Sorry, this feature is currently only supported with pnpm.`)
183 | }
184 | }
185 |
186 | async function copyTemplateFiles(extension, storiesPath) {
187 | // Copy the template files to the project root
188 | const packagePath = await getPackageDir('@storybook-vue/nuxt')
189 | const templateDir = path.join(packagePath, 'template', 'cli', extension)
190 | const targetDir = path.join(process.cwd(), storiesPath)
191 | copyFolderRecursive(templateDir, targetDir)
192 | // Copy the common assets to the project root
193 | const rootDir = path.join(fileURLToPath(import.meta.url), '../..')
194 | const commonAssetsDir = path.join(rootDir, '.storybook', 'rendererAssets/common')
195 | copyFolderRecursive(commonAssetsDir, targetDir)
196 | }
197 |
198 | function copyFolderRecursive(sourceFolder, destinationFolder) {
199 | // Create the destination folder if it doesn't exist
200 | if (!existsSync(destinationFolder))
201 | mkdirSync(destinationFolder)
202 |
203 | // Read the contents of the source folder
204 | const files = readdirSync(sourceFolder)
205 |
206 | // Loop through the files in the source folder
207 | for (const file of files) {
208 | const sourceFilePath = path.join(sourceFolder, file)
209 | const destinationFilePath = path.join(destinationFolder, file)
210 |
211 | // Get the file's stats to check if it's a directory or a file
212 | const stats = statSync(sourceFilePath)
213 |
214 | if (stats.isFile()) {
215 | // If it's a file, copy it to the destination folder
216 | copyFileSync(sourceFilePath, destinationFilePath)
217 | }
218 | else if (stats.isDirectory()) {
219 | // If it's a directory, recursively copy it
220 | copyFolderRecursive(sourceFilePath, destinationFilePath)
221 | }
222 | }
223 | }
224 |
225 | async function getPackageDir(frameworkPackageName) {
226 | // const packageJsonPath = join(frameworkPackageName, 'package.json')
227 |
228 | try {
229 | const require = createRequire(import.meta.url)
230 | const packageDir = path.dirname(require.resolve(path.join(frameworkPackageName, 'package.json'), { paths: [process.cwd()] }))
231 |
232 | return packageDir
233 | }
234 | catch (e) {
235 | logger.error(e)
236 | }
237 | throw new Error(`Cannot find ${frameworkPackageName},`)
238 | }
239 |
240 | async function initNuxtProject() {
241 | const isEmpty = readdirSync(process.cwd()).length === 0
242 | if (!isEmpty) {
243 | logger.error(' Directory is not empty')
244 | return true
245 | }
246 | const startProcess = spawn('npx', ['nuxi', 'init', '.'], {
247 | cwd: process.cwd(),
248 | stdio: 'inherit',
249 | })
250 |
251 | return new Promise((resolve, reject) => {
252 | startProcess.on('close', (code) => {
253 | if (code !== 0) {
254 | logger.error(`${CROSSMARK} Nuxt failed to init ${code}`)
255 | if (code === 1)
256 | resolve(true)
257 | else
258 | reject(code)
259 | }
260 | else {
261 | logger.log(`${CHECKMARK} Nuxt started successfully!`)
262 | resolve(true)
263 | }
264 | })
265 | })
266 | }
267 | async function installDependencies() {
268 | const installProcess = spawn(packageManager, ['install'], {
269 | cwd: process.cwd(),
270 | stdio: 'inherit',
271 | })
272 | return new Promise((resolve, reject) => {
273 | installProcess.on('close', (code) => {
274 | if (code !== 0) {
275 | logger.error(`${CROSSMARK} Package installation failed with code ${code}`)
276 | reject(code)
277 | }
278 | else {
279 | logger.log(`${CHECKMARK} Packages installed successfully!`)
280 | resolve(true)
281 | }
282 | })
283 | })
284 | }
285 |
286 | export { initStorybook, initNuxtProject as initNuxt, installDependencies }
287 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/playground/components/PiniaLogo.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
256 |
257 |
258 |
309 |
310 |
330 |
--------------------------------------------------------------------------------
/packages/storybook-nuxt/src/runtime/components/nuxt-link.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-statements-per-line */
2 | import type { ComputedRef, DefineComponent, PropType } from 'vue'
3 | import {
4 | computed, defineComponent, h, onBeforeUnmount, onMounted, ref, resolveComponent,
5 | } from 'vue'
6 | import { useRouter as useVueRouter } from 'vue-router'
7 | import type { RouteLocation, RouteLocationRaw } from 'vue-router'
8 | import {
9 | hasProtocol, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash,
10 | } from 'ufo'
11 |
12 | import {
13 | navigateTo, onNuxtReady, preloadRouteComponents, useNuxtApp, useRouter,
14 | } from 'nuxt/app'
15 |
16 | const firstNonUndefined = (...args: (T | undefined)[]) => args.find(arg => arg !== undefined)
17 |
18 | const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer'
19 |
20 | export interface NuxtLinkOptions {
21 | componentName?: string
22 | externalRelAttribute?: string | null
23 | activeClass?: string
24 | exactActiveClass?: string
25 | prefetchedClass?: string
26 | trailingSlash?: 'append' | 'remove'
27 | }
28 |
29 | export interface NuxtLinkProps {
30 | // Routing
31 | to?: RouteLocationRaw
32 | href?: RouteLocationRaw
33 | external?: boolean
34 | replace?: boolean
35 | custom?: boolean
36 |
37 | // Attributes
38 | target?: '_blank' | '_parent' | '_self' | '_top' | (string & object) | null
39 | rel?: string | null
40 | noRel?: boolean
41 |
42 | prefetch?: boolean
43 | noPrefetch?: boolean
44 |
45 | // Styling
46 | activeClass?: string
47 | exactActiveClass?: string
48 |
49 | // Vue Router's `` additional props
50 | ariaCurrentValue?: string
51 | }
52 |
53 | // Polyfills for Safari support
54 | // https://caniuse.com/requestidlecallback
55 | export const requestIdleCallback: Window['requestIdleCallback'] = import.meta.server
56 | ? (() => {}) as any
57 | : (globalThis.requestIdleCallback || ((cb) => {
58 | const start = Date.now()
59 | const idleDeadline = {
60 | didTimeout: false,
61 | timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
62 | }
63 | return setTimeout(() => { cb(idleDeadline) }, 1)
64 | }))
65 |
66 | export const cancelIdleCallback: Window['cancelIdleCallback'] = import.meta.server
67 | ? (() => {}) as any
68 | : (globalThis.cancelIdleCallback || ((id) => { clearTimeout(id) }))
69 |
70 | /*! @__NO_SIDE_EFFECTS__ */
71 | export function defineNuxtLink(options: NuxtLinkOptions) {
72 | const componentName = options.componentName || 'NuxtLink'
73 |
74 | const checkPropConflicts = (props: NuxtLinkProps, main: keyof NuxtLinkProps, sub: keyof NuxtLinkProps): void => {
75 | if (import.meta.dev && props[main] !== undefined && props[sub] !== undefined)
76 | console.warn(`[${componentName}] \`${main}\` and \`${sub}\` cannot be used together. \`${sub}\` will be ignored.`)
77 | }
78 | const resolveTrailingSlashBehavior = (
79 | to: RouteLocationRaw,
80 | resolve: (to: RouteLocationRaw) => RouteLocation & { href?: string },
81 | ): RouteLocationRaw | RouteLocation => {
82 | if (!to || (options.trailingSlash !== 'append' && options.trailingSlash !== 'remove'))
83 | return to
84 |
85 | const normalizeTrailingSlash = options.trailingSlash === 'append' ? withTrailingSlash : withoutTrailingSlash
86 | if (typeof to === 'string')
87 | return normalizeTrailingSlash(to, true)
88 |
89 | const path = 'path' in to ? to.path : resolve(to).path
90 |
91 | return {
92 | ...to,
93 | name: undefined, // named routes would otherwise always override trailing slash behavior
94 | path: normalizeTrailingSlash(path, true),
95 | }
96 | }
97 |
98 | return defineComponent({
99 | name: componentName,
100 | props: {
101 | // Routing
102 | to: {
103 | type: [String, Object] as PropType,
104 | default: undefined,
105 | required: false,
106 | },
107 | href: {
108 | type: [String, Object] as PropType,
109 | default: undefined,
110 | required: false,
111 | },
112 |
113 | // Attributes
114 | target: {
115 | type: String as PropType,
116 | default: undefined,
117 | required: false,
118 | },
119 | rel: {
120 | type: String as PropType,
121 | default: undefined,
122 | required: false,
123 | },
124 | noRel: {
125 | type: Boolean as PropType,
126 | default: undefined,
127 | required: false,
128 | },
129 |
130 | // Prefetching
131 | prefetch: {
132 | type: Boolean as PropType,
133 | default: undefined,
134 | required: false,
135 | },
136 | noPrefetch: {
137 | type: Boolean as PropType,
138 | default: undefined,
139 | required: false,
140 | },
141 |
142 | // Styling
143 | activeClass: {
144 | type: String as PropType,
145 | default: undefined,
146 | required: false,
147 | },
148 | exactActiveClass: {
149 | type: String as PropType,
150 | default: undefined,
151 | required: false,
152 | },
153 | prefetchedClass: {
154 | type: String as PropType,
155 | default: undefined,
156 | required: false,
157 | },
158 |
159 | // Vue Router's `` additional props
160 | replace: {
161 | type: Boolean as PropType,
162 | default: undefined,
163 | required: false,
164 | },
165 | ariaCurrentValue: {
166 | type: String as PropType,
167 | default: undefined,
168 | required: false,
169 | },
170 |
171 | // Edge cases handling
172 | external: {
173 | type: Boolean as PropType,
174 | default: undefined,
175 | required: false,
176 | },
177 |
178 | // Slot API
179 | custom: {
180 | type: Boolean as PropType,
181 | default: undefined,
182 | required: false,
183 | },
184 | },
185 | setup(props, { slots }) {
186 | const router = useRouter() ?? useVueRouter()
187 | // Resolving `to` value from `to` and `href` props
188 | const to: ComputedRef = computed(() => {
189 | checkPropConflicts(props, 'to', 'href')
190 |
191 | const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
192 |
193 | return resolveTrailingSlashBehavior(path, router.resolve)
194 | })
195 |
196 | // Resolving link type
197 | const isExternal = computed(() => {
198 | // External prop is explicitly set
199 | if (props.external)
200 | return true
201 |
202 | // When `target` prop is set, link is external
203 | if (props.target && props.target !== '_self')
204 | return true
205 |
206 | // When `to` is a route object then it's an internal link
207 | if (typeof to.value === 'object')
208 | return false
209 |
210 | return to.value === '' || hasProtocol(to.value, { acceptRelative: true })
211 | })
212 |
213 | // Prefetching
214 | const prefetched = ref(false)
215 | const el = import.meta.server ? undefined : ref(null)
216 | const elRef = import.meta.server
217 | ? undefined
218 | : (ref: any) => { el!.value = props.custom ? ref?.$el?.nextElementSibling : ref?.$el }
219 |
220 | if (import.meta.client) {
221 | checkPropConflicts(props, 'prefetch', 'noPrefetch')
222 | const shouldPrefetch = props.prefetch !== false && props.noPrefetch !== true && props.target !== '_blank' && !isSlowConnection()
223 | if (shouldPrefetch) {
224 | const nuxtApp = useNuxtApp()
225 | let idleId: number
226 | let unobserve: (() => void) | null = null
227 | onMounted(() => {
228 | const observer = useObserver()
229 | onNuxtReady(() => {
230 | idleId = requestIdleCallback(() => {
231 | if (el?.value?.tagName) {
232 | unobserve = observer!.observe(el.value as HTMLElement, async () => {
233 | unobserve?.()
234 | unobserve = null
235 |
236 | const path = typeof to.value === 'string' ? to.value : router.resolve(to.value).fullPath
237 | await Promise.all([
238 | nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}),
239 | !isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
240 | ])
241 | prefetched.value = true
242 | })
243 | }
244 | })
245 | })
246 | })
247 | onBeforeUnmount(() => {
248 | if (idleId)
249 | cancelIdleCallback(idleId)
250 | unobserve?.()
251 | unobserve = null
252 | })
253 | }
254 | }
255 |
256 | return () => {
257 | if (!isExternal.value) {
258 | const routerLinkProps: Record = {
259 | ref: elRef,
260 | to: to.value,
261 | activeClass: props.activeClass || options.activeClass,
262 | exactActiveClass: props.exactActiveClass || options.exactActiveClass,
263 | replace: props.replace,
264 | ariaCurrentValue: props.ariaCurrentValue,
265 | custom: props.custom,
266 | }
267 |
268 | // `custom` API cannot support fallthrough attributes as the slot
269 | // may render fragment or text root nodes (#14897, #19375)
270 | if (!props.custom) {
271 | if (prefetched.value)
272 | routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
273 |
274 | routerLinkProps.rel = props.rel
275 | }
276 |
277 | // Internal link
278 | return h(
279 | resolveComponent('RouterLink'),
280 | routerLinkProps,
281 | slots.default,
282 | )
283 | }
284 |
285 | // Resolves `to` value if it's a route location object
286 | // converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
287 | const href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null
288 |
289 | // Resolves `target` value
290 | const target = props.target || null
291 |
292 | // Resolves `rel`
293 | checkPropConflicts(props, 'noRel', 'rel')
294 | const rel = (props.noRel)
295 | ? null
296 | // converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`)
297 | : firstNonUndefined(props.rel, options.externalRelAttribute, href ? DEFAULT_EXTERNAL_REL_ATTRIBUTE : '') || null
298 |
299 | const navigate = () => navigateTo(href, { replace: props.replace })
300 |
301 | // https://router.vuejs.org/api/#custom
302 | if (props.custom) {
303 | if (!slots.default)
304 | return null
305 |
306 | return slots.default({
307 | href,
308 | navigate,
309 | get route() {
310 | if (!href)
311 | return undefined
312 |
313 | const url = parseURL(href)
314 | return {
315 | path: url.pathname,
316 | fullPath: url.pathname,
317 | get query() { return parseQuery(url.search) },
318 | hash: url.hash,
319 | // stub properties for compat with vue-router
320 | params: {},
321 | name: undefined,
322 | matched: [],
323 | redirectedFrom: undefined,
324 | meta: {},
325 | href,
326 | }
327 | },
328 | rel,
329 | target,
330 | isExternal: isExternal.value,
331 | isActive: false,
332 | isExactActive: false,
333 | })
334 | }
335 |
336 | return h('a', {
337 | ref: el, href, rel, target,
338 | }, slots.default?.())
339 | }
340 | },
341 | }) as unknown as DefineComponent
342 | }
343 |
344 | export default defineNuxtLink({ componentName: 'NuxtLink' })
345 |
346 | // --- Prefetching utils ---
347 | type CallbackFn = () => void
348 | type ObserveFn = (element: Element, callback: CallbackFn) => () => void
349 |
350 | function useObserver(): { observe: ObserveFn } | undefined {
351 | if (import.meta.server)
352 | return
353 |
354 | const nuxtApp = useNuxtApp()
355 | if (nuxtApp._observer)
356 | return nuxtApp._observer
357 |
358 | let observer: IntersectionObserver | null = null
359 |
360 | const callbacks = new Map()
361 |
362 | const observe: ObserveFn = (element, callback) => {
363 | if (!observer) {
364 | observer = new IntersectionObserver((entries) => {
365 | for (const entry of entries) {
366 | const callback = callbacks.get(entry.target)
367 | const isVisible = entry.isIntersecting || entry.intersectionRatio > 0
368 | if (isVisible && callback)
369 | callback()
370 | }
371 | })
372 | }
373 | callbacks.set(element, callback)
374 | observer.observe(element)
375 | return () => {
376 | callbacks.delete(element)
377 | observer!.unobserve(element)
378 | if (callbacks.size === 0) {
379 | observer!.disconnect()
380 | observer = null
381 | }
382 | }
383 | }
384 |
385 | const _observer = nuxtApp._observer = {
386 | observe,
387 | }
388 |
389 | return _observer
390 | }
391 |
392 | function isSlowConnection() {
393 | if (import.meta.server)
394 | return
395 |
396 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
397 | const cn = (navigator as any).connection as { saveData: boolean; effectiveType: string } | null
398 | if (cn && (cn.saveData || /2g/.test(cn.effectiveType)))
399 | return true
400 | return false
401 | }
402 |
--------------------------------------------------------------------------------