diff --git a/.babelrc.js b/.babelrc.js index 252bdc33b896..13e7b0a37c25 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -12,13 +12,30 @@ const withTests = { ], }; +// type BabelMode = 'cjs' | 'esm' | 'modern'; + +const modules = process.env.BABEL_MODE === 'cjs' ? 'auto' : false; + +// FIXME: optional chaining introduced in chrome 80, not supported by wepback4 +// https://github.com/webpack/webpack/issues/10227#issuecomment-642734920 +const targets = process.env.BABEL_MODE === 'modern' ? { chrome: '79' } : 'defaults'; + module.exports = { ignore: [ './lib/codemod/src/transforms/__testfixtures__', './lib/postinstall/src/__testfixtures__', ], presets: [ - ['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '3' }], + [ + '@babel/preset-env', + { + shippedProposals: true, + useBuiltIns: 'usage', + corejs: '3', + targets, + modules, + }, + ], '@babel/preset-typescript', '@babel/preset-react', '@babel/preset-flow', @@ -52,7 +69,16 @@ module.exports = { { test: './lib', presets: [ - ['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '3' }], + [ + '@babel/preset-env', + { + shippedProposals: true, + useBuiltIns: 'usage', + corejs: '3', + modules, + targets, + }, + ], '@babel/preset-react', ], plugins: [ @@ -62,7 +88,6 @@ module.exports = { ['@babel/plugin-proposal-class-properties', { loose: true }], 'babel-plugin-macros', ['emotion', { sourceMap: true, autoLabel: true }], - '@babel/plugin-transform-react-constant-elements', 'babel-plugin-add-react-displayname', ], env: { @@ -72,6 +97,11 @@ module.exports = { { test: [ './lib/node-logger', + './lib/core', + './lib/core-common', + './lib/core-server', + './lib/builder-webpack4', + './lib/builder-webpack5', './lib/codemod', './addons/storyshots', '**/src/server/**', @@ -84,8 +114,9 @@ module.exports = { shippedProposals: true, useBuiltIns: 'usage', targets: { - node: '8.11', + node: '10', }, + modules, corejs: '3', }, ], @@ -105,5 +136,22 @@ module.exports = { test: withTests, }, }, + { + test: ['**/virtualModuleEntry.template.js'], + presets: [ + [ + '@babel/preset-env', + { + shippedProposals: true, + useBuiltIns: 'usage', + targets: { + node: '10', + }, + corejs: '3', + modules: false, + }, + ], + ], + }, ], }; diff --git a/.bettercodehub.yml b/.bettercodehub.yml deleted file mode 100644 index 65ad2d8059ef..000000000000 --- a/.bettercodehub.yml +++ /dev/null @@ -1,16 +0,0 @@ -component_depth: 2 -languages: -- javascript - -- name: javascript - production: - exclude: - - .*\.test\.js - - .*\/__test__\/.*\.js - - .*\/__mock__\/.*\.js - - .*\.stories\.js - test: - include: - - .*\.test\.js - - .*\/__test__\/.*\.js - - .*\.storyshot diff --git a/.circleci/config.yml b/.circleci/config.yml index 3658e56fe2fe..b5259168ea40 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,62 +1,116 @@ version: 2.1 -aliases: - - &defaults +executors: + sb_node_12_classic: + parameters: + class: + description: The Resource class + type: enum + enum: ['small', 'medium', 'large', 'xlarge'] + default: 'medium' working_directory: /tmp/storybook docker: - - image: circleci/node:10-browsers + - image: circleci/node:12 + environment: + NODE_OPTIONS: --max_old_space_size=3076 + resource_class: <> + sb_node_12_browsers: + parameters: + class: + description: The Resource class + type: enum + enum: ['small', 'medium', 'large', 'xlarge'] + default: 'medium' + working_directory: /tmp/storybook + docker: + - image: circleci/node:12-browsers + environment: + NODE_OPTIONS: --max_old_space_size=3076 + resource_class: <> + sb_cypress_6_node_12: + parameters: + class: + description: The Resource class + type: enum + enum: ['small', 'medium', 'large', 'xlarge'] + default: 'medium' + working_directory: /tmp/storybook + docker: + # ⚠️ The Cypress docker image is based on Node.js one so be careful when updating it because it can also + # cause an upgrade of the Node. + - image: cypress/included:6.8.0 + environment: + NODE_OPTIONS: --max_old_space_size=3076 + resource_class: <> + +orbs: + git-shallow-clone: guitarrapc/git-shallow-clone@2.0.3 + +commands: + ensure-pr-is-labeled-with: + description: 'A command looking for the labels set on the PR associated to this workflow and checking it contains the label given as parameter' + parameters: + label: + type: string + steps: + - run: + name: Check if PR is labeled with "<< parameters.label >>" + command: | + apt-get -y install jq + + PR_NUMBER=$(echo "$CIRCLE_PULL_REQUEST" | sed "s/.*\/pull\///") + echo "PR_NUMBER: $PR_NUMBER" + + API_GITHUB="https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" + PR_REQUEST_URL="$API_GITHUB/pulls/$PR_NUMBER" + PR_RESPONSE=$(curl -H "Authorization: token $GITHUB_TOKEN_STORYBOOK_BOT_READ_REPO" "$PR_REQUEST_URL") + + + if [ $(echo $PR_RESPONSE | jq '.labels | map(select(.name == "<< parameters.label >>")) | length') -ge 1 ] || + ( [ $(echo $PR_RESPONSE | jq '.labels | length') -ge 1 ] && [ "<< parameters.label >>" == "*" ]) + then + echo "🚀 The PR is labelled with '<< parameters.label >>', job will continue!" + else + echo "🏁 The PR isn't labelled with '<< parameters.label >>' so this job will end at the current step." + circleci-agent step halt + fi jobs: - install: - <<: *defaults + build: + executor: + class: xlarge + name: sb_node_12_classic steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - restore_cache: - name: Restore core dependencies cache + name: Restore Yarn cache keys: - - core-dependencies-v5-{{ checksum "yarn.lock" }} - - core-dependencies-v5- + - build-yarn-2-cache-v1--{{ checksum "yarn.lock" }} - run: name: Install dependencies - command: yarn install - - run: - name: Check that yarn.lock is not corrupted - command: yarn repo-dirty-check - - save_cache: - name: Cache core dependencies - key: core-dependencies-v5-{{ checksum "yarn.lock" }} - paths: - - node_modules - - persist_to_workspace: - root: . - paths: - - node_modules - - examples - - addons - - dev-kits - - app - - lib - build: - <<: *defaults - steps: - - checkout - - attach_workspace: - at: . + command: yarn install --immutable - run: name: Bootstrap command: yarn bootstrap --core + - save_cache: + name: Save Yarn cache + key: build-yarn-2-cache-v1--{{ checksum "yarn.lock" }} + paths: + - ~/.yarn/berry/cache - persist_to_workspace: root: . paths: - examples + - node_modules - addons - - dev-kits - app - lib chromatic: - <<: *defaults - parallelism: 11 + executor: sb_node_12_browsers + parallelism: 4 steps: + # Keep using default checkout because Chromatic needs some git history to work properly - checkout - attach_workspace: at: . @@ -65,9 +119,12 @@ jobs: command: | yarn run-chromatics packtracker: - <<: *defaults + executor: + class: medium + name: sb_node_12_browsers steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -76,10 +133,13 @@ jobs: cd examples/official-storybook yarn packtracker examples: - <<: *defaults - parallelism: 11 + executor: + class: medium + name: sb_node_12_browsers + parallelism: 4 steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -91,9 +151,12 @@ jobs: paths: - built-storybooks publish: - <<: *defaults + executor: + class: medium + name: sb_node_12_classic steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -103,15 +166,24 @@ jobs: root: . paths: - .verdaccio-cache - examples-v2: - docker: - - image: cypress/included:4.7.0 - environment: - TERM: xterm - working_directory: /tmp/storybook - parallelism: 10 + e2e-tests-extended: + executor: + class: medium + name: sb_cypress_6_node_12 + parallelism: 4 steps: - - checkout + - when: + condition: + and: + - not: + equal: [main, << pipeline.git.branch >>] + - not: + equal: [next, << pipeline.git.branch >>] + steps: + - ensure-pr-is-labeled-with: + label: 'run e2e extended test suite' + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -119,58 +191,95 @@ jobs: command: yarn local-registry --port 6000 --open background: true - run: - name: wait for registry + name: Wait for registry command: yarn wait-on http://localhost:6000 - run: - name: set registry - command: yarn config set registry http://localhost:6000/ + name: Run E2E tests + command: yarn test:e2e-framework --clean --all --skip angular11 --skip angular --skip vue3 --skip web_components_typescript --skip cra + no_output_timeout: 5m + - store_artifacts: + path: /tmp/storybook/cypress + destination: cypress + e2e-tests-core: + executor: + class: medium + name: sb_cypress_6_node_12 + parallelism: 2 + steps: + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' + - attach_workspace: + at: . + - run: + name: Running local registry + command: yarn local-registry --port 6000 --open + background: true - run: - name: test local registry - command: yarn info @storybook/core + name: Wait for registry + command: yarn wait-on http://localhost:6000 - run: - name: run e2e tests - command: yarn test:e2e-framework + name: Run E2E tests + # Do not test CRA here because it's done in PnP part + # TODO: Remove `web_components_typescript` as soon as Lit 2 stable is released + command: yarn test:e2e-framework vue3 angular angular11 web_components_typescript web_components_lit2 + no_output_timeout: 5m - store_artifacts: path: /tmp/storybook/cypress destination: cypress - examples-v2-yarn-2: - docker: - - image: cypress/included:4.7.0 - environment: - TERM: xterm + cra-bench: + executor: + class: medium + name: sb_cypress_6_node_12 working_directory: /tmp/storybook - # parallelism: 10 steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: - name: running local registry + name: Running local registry command: yarn local-registry --port 6000 --open background: true - run: - name: wait for registry + name: Wait for registry command: yarn wait-on http://localhost:6000 - run: - name: set registry - command: yarn config set registry http://localhost:6000/ + name: Run @storybook/bench on a CRA project + command: | + cd .. + npx create-react-app cra-bench + cd cra-bench + npx @storybook/bench 'npx sb init' --label cra --extra-flags "--modern" + e2e-tests-pnp: + executor: + class: medium + name: sb_cypress_6_node_12 + working_directory: /tmp/storybook + steps: + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' + - attach_workspace: + at: . - run: - name: test local registry - command: yarn info @storybook/core + name: Running local registry + command: yarn local-registry --port 6000 --open + background: true + - run: + name: Wait for registry + command: yarn wait-on http://localhost:6000 - run: name: run e2e tests - command: yarn test:e2e-framework yarn2Cra + command: yarn test:e2e-framework --pnp sfcVue cra - store_artifacts: path: /tmp/storybook/cypress destination: cypress - e2e: - working_directory: /tmp/storybook - docker: - - image: cypress/included:4.7.0 - environment: - TERM: xterm + e2e-tests-examples: + executor: + class: small + name: sb_cypress_6_node_12 steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -182,12 +291,21 @@ jobs: command: yarn await-serve-storybooks - run: name: cypress run - command: yarn test:e2e - + command: yarn test:e2e-examples + - store_artifacts: + path: /tmp/storybook/cypress + destination: cypress smoke-tests: - <<: *defaults + executor: + class: medium + name: sb_node_12_browsers + environment: + # Disable ESLint when running smoke tests to improve perf + As of CRA 4.0.3, CRA kitchen sinks are throwing + # because of some ESLint warnings, related to: https://github.com/facebook/create-react-app/pull/10590 + DISABLE_ESLINT_PLUGIN: 'true' steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -220,90 +338,65 @@ jobs: command: | cd examples/ember-cli yarn storybook --smoke-test --quiet - - run: - name: Run marko-cli (smoke test) - command: | - cd examples/marko-cli - yarn storybook --smoke-test --quiet - run: name: Run official-storybook (smoke test) command: | cd examples/official-storybook yarn storybook --smoke-test --quiet - - run: - name: Run mithril kitchen-sink (smoke test) - command: | - cd examples/mithril-kitchen-sink - yarn storybook --smoke-test --quiet - - run: - name: Run riot kitchen-sink (smoke test) - command: | - cd examples/riot-kitchen-sink - yarn storybook --smoke-test --quiet - run: name: Run preact kitchen-sink (smoke test) command: | cd examples/preact-kitchen-sink yarn storybook --smoke-test --quiet - run: - name: Run cra reac15 (smoke test) + name: Run cra react15 (smoke test) command: | cd examples/cra-react15 yarn storybook --smoke-test --quiet frontpage: - <<: *defaults + executor: sb_node_12_browsers steps: - - checkout - - restore_cache: - name: Restore core dependencies cache - keys: - - core-dependencies-v5-{{ checksum "yarn.lock" }} + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - run: name: Install dependencies - command: yarn bootstrap --install + command: yarn install --immutable - run: name: Trigger build command: ./scripts/build-frontpage.js - docs: - <<: *defaults - steps: - - checkout - - run: - name: Install dependencies - command: | - cd docs - yarn install - - run: - name: Build docs - command: | - cd docs - yarn build lint: - <<: *defaults + executor: + class: small + name: sb_node_12_classic steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: name: Lint command: yarn lint - test: - <<: *defaults + unit-tests: + executor: sb_node_12_browsers steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: name: Test - command: yarn test --coverage --w2 --core + command: yarn test --coverage --runInBand --ci - persist_to_workspace: root: . paths: - coverage coverage: - <<: *defaults + executor: + class: small + name: sb_node_12_browsers steps: - - checkout + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' - attach_workspace: at: . - run: @@ -313,17 +406,14 @@ jobs: workflows: test: jobs: - - install - - build: - requires: - - install + - build - lint: requires: - build - examples: requires: - build - - e2e: + - e2e-tests-examples: requires: - examples - smoke-tests: @@ -332,25 +422,30 @@ workflows: - packtracker: requires: - build - - test: + - unit-tests: requires: - build - coverage: requires: - - test + - unit-tests - chromatic: requires: - examples - publish: requires: - build - - examples-v2: + - e2e-tests-extended: + requires: + - publish + - e2e-tests-core: + requires: + - publish + - e2e-tests-pnp: requires: - publish - - examples-v2-yarn-2: + - cra-bench: requires: - publish deploy: jobs: - - docs - frontpage diff --git a/.eslintignore b/.eslintignore index a2f20ebea5a3..55d571343afc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,7 +7,11 @@ docs/public storybook-static built-storybooks lib/cli/test +lib/manager-webpack4/prebuilt +lib/manager-webpack5/prebuilt +lib/core-server/prebuilt lib/codemod/src/transforms/__testfixtures__ +lib/components/src/controls/react-editable-json-tree scripts/storage *.bundle.js *.js.map @@ -18,7 +22,6 @@ examples/cra-ts-kitchen-sink/*.json examples/cra-ts-kitchen-sink/public/* examples/cra-ts-essentials/*.json examples/cra-ts-essentials/public/* -examples/rax-kitchen-sink/src/document/* ember-output .yarn !.remarkrc.js diff --git a/.eslintrc.js b/.eslintrc.js index 99d83c78c2f6..6c1acdeef112 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,9 @@ module.exports = { root: true, extends: ['@storybook/eslint-config-storybook'], + rules: { + '@typescript-eslint/ban-ts-comment': 'warn', + }, overrides: [ { files: [ @@ -10,8 +13,6 @@ module.exports = { '**/*.test.*', '**/*.stories.*', '**/storyshots/**/stories/**', - 'docs/src/new-components/lib/StoryLinkWrapper.js', - 'docs/src/stories/**', ], rules: { '@typescript-eslint/no-empty-function': 'off', @@ -37,7 +38,9 @@ module.exports = { { files: ['**/*.tsx', '**/*.ts'], rules: { + 'react/require-default-props': 'off', 'react/prop-types': 'off', // we should use types + 'react/forbid-prop-types': 'off', // we should use types 'no-dupe-class-members': 'off', // this is called overloads in typescript }, }, @@ -49,5 +52,11 @@ module.exports = { 'spaced-comment': 'off', }, }, + { + files: ['**/mithril/**/*'], + rules: { + 'react/no-unknown-property': 'off', // Need to deactivate otherwise eslint replaces some unknown properties with React ones + }, + }, ], }; diff --git a/.gitattributes b/.gitattributes index 5ea62bf2933c..fb467cb53219 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -.yarn/releases/yarn-*.js linguist-generated=true +/.yarn/** linguist-generated diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 79a552730113..000000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,47 +0,0 @@ -.circleci/ @ndelangen -.github/ @danielduan - -/addons/a11y/ @jbovenschen @codebyalex -/addons/actions/ @rhalff -/addons/backgrounds/ @ndelangen -/addons/events/ @z4o4z @ndelangen -/addons/graphql/ @mnmtanish -/addons/info/ @theinterned @z4o4z @UsulPro @dangreenisrael -/addons/jest/ @renaudtertrais -/addons/knobs/ @alexandrebodin @theinterned @leonrodenburg @alterx -/addons/links/ @ndelangen -/addons/notes/ @alexandrebodin -/addons/options/ @UsulPro -/addons/storyshots/ @igor-dv @thomasbertet -/addons/storysource/ @igor-dv -/addons/viewport/ @saponifi3d - -/app/angular/ @alterx @igor-dv -/app/react/ @xavcz @shilman @thomasbertet -/app/vue/ @thomasbertet @kazupon -/app/svelte/ @plumpNation - -/docs/ @ndelangen @shilman - -/examples/angular-cli/ @igor-dv @alterx -/examples/cra-kitchen-sink/ @ndelangen @UsulPro -/examples/cra-ts-kitchen-sink/ @mucsi96 -/examples/official-storybook/ @UsulPro -/examples/vue-kitchen-sink/ @igor-dv @alexandrebodin -/examples/svelte-kitchen-sink/ @plumpNation - -/examples-native/crna-kitchen-sink/ @Gongreg - -/lib/addons/ @ndelangen @theinterned -/lib/channel-postmessage/ @mnmtanish @ndelangen -/lib/channel-websocket/ @mnmtanish @ndelangen -/lib/channels/ @mnmtanish @ndelangen -/lib/cli/ @ndelangen @shilman @stijnkoopal -/lib/client-logger/ @dangreenisrael -/lib/codemod/ @aaronmcadam @ndelangen -/lib/components/ @ndelangen @tmeasday -/lib/core/ @tmeasday @igor-dv @alterx -/lib/node-logger/ @dangreenisrael -/lib/ui/ @tmeasday @igor-dv @ndelangen - -/scripts/ @ndelangen @igor-dv diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1f1a7637fbd9..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,47 +0,0 @@ -If you are reporting a bug or requesting support, start here: -### Bug or support request summary - -_Please provide issue details here - What did you expect to happen? What happened instead?_ - -### Steps to reproduce - -_Please provide necessary steps for reproduction of this issue. Describe the exact steps a maintainer has to take to make the problem occur. If the problem is non-trivial to reproduce, please link a repository or provide some code snippets._ - -_(A screencast can be useful for visual bugs, but it is not a substitute for a textual description.)_ - -### Please specify which version of Storybook and optionally any affected addons that you're running - -- @storybook/react x.x.x -- @storybook/addon-something x.x.x - -### Affected platforms - -- _If UI related, please indicate browser, OS, and version_ -- _If dependency related, please include relevant version numbers_ -- _If developer tooling related, please include the platform information_ - -### Screenshots / Screencast / Code Snippets (Optional) - -```js -// code here -``` -End bug report support request - delete the rest below - - -If you are creating a issue to track work to be completed, start here: -### Work summary - -_Please provide a description of the work to be completed here - Include some context as to why something needs to be done and link any related tickets._ - -### Where to start - -_Please list the file(s) a contributor needs to figure out where to start work and include any docs or tutorials that may be applicable._ - -### Acceptance criteria - -_Please include a checklist of the requirements necessary to close this ticket. The work should be narrowly scoped and limited to a few hours worth by an experienced developer at the most._ - -### Who to contact - -_Add yourself and/or people who are familiar with the code changes and requirements. These people should be able to review the completed code._ -End work issue - please tag this issue with the correct status and type labels diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8a63d2211dbd..3381dd30e860 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,30 +1,18 @@ --- -name: Bug report -about: Create a report to help us improve - +name: Bug report 🐞 +about: Something is broken and you have a reliable reproduction? Let us know here. For questions, please use "Question" below. +labels: needs triage, bug --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Code snippets** -If applicable, add code samples to help explain your problem. +Please create a reproduction by running `npx sb@next repro` and following the instructions. Read our [documentation](https://storybook.js.org/docs/react/contribute/how-to-reproduce) to learn more about creating reproductions. +Paste your repository and deployed reproduction here. We prioritize issues with reproductions over those without. -**System:** -Please paste the results of `npx -p @storybook/cli@next sb info` here. +**System** +Please paste the results of `npx sb@next info` here. **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..d7cb62f6095c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation 📚 + url: https://storybook.js.org/docs/ + about: Check out the official docs for answers to common questions + - name: Questions & discussions 🤔 + url: https://github.com/storybookjs/storybook/discussions + about: Ask questions, request features & discuss RFCs + - name: Community Discord 💬 + url: https://discord.gg/storybook + about: Community discussions, interactive support, contributor help diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5bdf5059fbf6..90c0ecccd1b2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,19 +1,19 @@ --- -name: Feature request +name: Feature request 💡 about: Suggest an idea for this project - +labels: needs triage, feature request --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +**Is your feature request related to a problem? Please describe** +A clear and concise description of the problem. E.g. I'm always frustrated when [...] **Describe the solution you'd like** -A clear and concise description of what you want to happen. +What would you like to see added to Storybook to solve problem? **Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +Any alternative solutions or features you've considered. -**Are you able to assist bring the feature to reality?** +**Are you able to assist to bring the feature to reality?** no | yes, I can... **Additional context** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 563677c8026f..6cf54b24835e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,7 +12,7 @@ If your answer is yes to any of these, please make sure to include it in your PR + + diff --git a/addons/docs/src/frameworks/svelte/sourceDecorator.test.ts b/addons/docs/src/frameworks/svelte/sourceDecorator.test.ts new file mode 100644 index 000000000000..6c55200e21fe --- /dev/null +++ b/addons/docs/src/frameworks/svelte/sourceDecorator.test.ts @@ -0,0 +1,47 @@ +import { Args } from '@storybook/api'; +import { generateSvelteSource } from './sourceDecorator'; + +expect.addSnapshotSerializer({ + print: (val: any) => val, + test: (val) => typeof val === 'string', +}); + +function generateForArgs(args: Args, slotProperty: string = null) { + return generateSvelteSource({ name: 'Component' }, args, {}, slotProperty); +} + +describe('generateSvelteSource', () => { + test('boolean true', () => { + expect(generateForArgs({ bool: true })).toMatchInlineSnapshot(``); + }); + test('boolean false', () => { + expect(generateForArgs({ bool: false })).toMatchInlineSnapshot(``); + }); + test('null property', () => { + expect(generateForArgs({ propnull: null })).toMatchInlineSnapshot(``); + }); + test('string property', () => { + expect(generateForArgs({ str: 'mystr' })).toMatchInlineSnapshot(``); + }); + test('number property', () => { + expect(generateForArgs({ count: 42 })).toMatchInlineSnapshot(``); + }); + test('object property', () => { + expect(generateForArgs({ obj: { x: true } })).toMatchInlineSnapshot( + `` + ); + }); + test('multiple properties', () => { + expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); + }); + test('slot property', () => { + expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, 'content')).toMatchInlineSnapshot(` + + xyz + + `); + }); + test('component is not set', () => { + expect(generateSvelteSource(null, null, null, null)).toBeNull(); + }); +}); diff --git a/addons/docs/src/frameworks/svelte/sourceDecorator.ts b/addons/docs/src/frameworks/svelte/sourceDecorator.ts new file mode 100644 index 000000000000..40e96c0cbd1e --- /dev/null +++ b/addons/docs/src/frameworks/svelte/sourceDecorator.ts @@ -0,0 +1,171 @@ +import { addons, StoryContext } from '@storybook/addons'; +import { ArgTypes, Args } from '@storybook/api'; + +import { SourceType, SNIPPET_RENDERED } from '../../shared'; + +/** + * Check if the sourcecode should be generated. + * + * @param context StoryContext + */ +const skipSourceRender = (context: StoryContext) => { + const sourceParams = context?.parameters.docs?.source; + const isArgsStory = context?.parameters.__isArgsStory; + + // always render if the user forces it + if (sourceParams?.type === SourceType.DYNAMIC) { + return false; + } + + // never render if the user is forcing the block to render code, or + // if the user provides code, or if it's not an args story. + return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; +}; + +/** + * Transform a key/value to a svelte declaration as string. + * + * Default values are ommited + * + * @param key Key + * @param value Value + * @param argTypes Component ArgTypes + */ +function toSvelteProperty(key: string, value: any, argTypes: ArgTypes): string { + if (value === undefined || value === null) { + return null; + } + + // default value ? + if (argTypes[key] && argTypes[key].defaultValue === value) { + return null; + } + + if (value === true) { + return key; + } + + if (typeof value === 'string') { + return `${key}=${JSON.stringify(value)}`; + } + + return `${key}={${JSON.stringify(value)}}`; +} + +/** + * Extract a component name. + * + * @param component Component + */ +function getComponentName(component: any): string { + if (component == null) { + return null; + } + + const { __docgen = {} } = component; + let { name } = __docgen; + + if (!name) { + return component.name; + } + + if (name.endsWith('.svelte')) { + name = name.substring(0, name.length - 7); + } + return name; +} + +/** + * Generate a svelte template. + * + * @param component Component + * @param args Args + * @param argTypes ArgTypes + * @param slotProperty Property used to simulate a slot + */ +export function generateSvelteSource( + component: any, + args: Args, + argTypes: ArgTypes, + slotProperty: string +): string { + const name = getComponentName(component); + + if (!name) { + return null; + } + + const props = Object.entries(args) + .filter(([k]) => k !== slotProperty) + .map(([k, v]) => toSvelteProperty(k, v, argTypes)) + .filter((p) => p) + .join(' '); + + const slotValue = slotProperty ? args[slotProperty] : null; + + if (slotValue) { + return `<${name} ${props}>\n ${slotValue}\n`; + } + + return `<${name} ${props}/>`; +} + +/** + * Check if the story component is a wrapper to the real component. + * + * A component can be annoted with @wrapper to indicate that + * it's just a wrapper for the real tested component. If it's the case + * then the code generated references the real component, not the wrapper. + * + * moreover, a wrapper can annotate a property with @slot : this property + * is then assumed to be an alias to the default slot. + * + * @param component Component + */ +function getWrapperProperties(component: any) { + const { __docgen } = component; + if (!__docgen) { + return { wrapper: false }; + } + + // the component should be declared as a wrapper + if (!__docgen.keywords.find((kw: any) => kw.name === 'wrapper')) { + return { wrapper: false }; + } + + const slotProp = __docgen.data.find((prop: any) => + prop.keywords.find((kw: any) => kw.name === 'slot') + ); + return { wrapper: true, slotProperty: slotProp?.name as string }; +} + +/** + * Svelte source decorator. + * @param storyFn Fn + * @param context StoryContext + */ +export const sourceDecorator = (storyFn: any, context: StoryContext) => { + const story = storyFn(); + + if (skipSourceRender(context)) { + return story; + } + + const channel = addons.getChannel(); + + const { parameters = {}, args = {} } = context || {}; + let { Component: component = {} } = story; + + const { wrapper, slotProperty } = getWrapperProperties(component); + if (wrapper) { + component = parameters.component; + } + + const source = generateSvelteSource(component, args, context?.argTypes, slotProperty); + + if (source) { + channel.emit(SNIPPET_RENDERED, (context || {}).id, source); + } + + return story; +}; diff --git a/addons/docs/src/frameworks/svelte/svelte-docgen-loader.ts b/addons/docs/src/frameworks/svelte/svelte-docgen-loader.ts new file mode 100644 index 000000000000..fcd09698431c --- /dev/null +++ b/addons/docs/src/frameworks/svelte/svelte-docgen-loader.ts @@ -0,0 +1,95 @@ +import svelteDoc from 'sveltedoc-parser'; +import dedent from 'ts-dedent'; +import * as path from 'path'; +import * as fs from 'fs'; +import { getOptions } from 'loader-utils'; +import { preprocess } from 'svelte/compiler'; +import { logger } from '@storybook/node-logger'; + +// From https://github.com/sveltejs/svelte/blob/8db3e8d0297e052556f0b6dde310ef6e197b8d18/src/compiler/compile/utils/get_name_from_filename.ts +// Copied because it is not exported from the compiler +function getNameFromFilename(filename: string) { + if (!filename) return null; + + const parts = filename.split(/[/\\]/).map(encodeURI); + + if (parts.length > 1) { + const index_match = parts[parts.length - 1].match(/^index(\.\w+)/); + if (index_match) { + parts.pop(); + parts[parts.length - 1] += index_match[1]; + } + } + + const base = parts + .pop() + .replace(/%/g, 'u') + .replace(/\.[^.]+$/, '') + .replace(/[^a-zA-Z_$0-9]+/g, '_') + .replace(/^_/, '') + .replace(/_$/, '') + .replace(/^(\d)/, '_$1'); + + if (!base) { + throw new Error(`Could not derive component name from file ${filename}`); + } + + return base[0].toUpperCase() + base.slice(1); +} + +/** + * webpack loader for sveltedoc-parser + * @param source raw svelte component + */ +export default async function svelteDocgen(source: string) { + // eslint-disable-next-line no-underscore-dangle + const { resource } = this._module; + const svelteOptions: any = { ...getOptions(this) }; + + const { preprocess: preprocessOptions, logDocgen = false } = svelteOptions; + + let docOptions; + if (preprocessOptions) { + const src = fs.readFileSync(resource).toString(); + + const { code: fileContent } = await preprocess(src, preprocessOptions); + docOptions = { + fileContent, + }; + } else { + docOptions = { filename: resource }; + } + + // set SvelteDoc options + const options = { + ...docOptions, + version: 3, + }; + + let docgen = ''; + + try { + const componentDoc = await svelteDoc.parse(options); + + // get filename for source content + const file = path.basename(resource); + + // populate filename in docgen + componentDoc.name = path.basename(file); + + const componentName = getNameFromFilename(resource); + + docgen = dedent` + + ${componentName}.__docgen = ${JSON.stringify(componentDoc)}; + `; + } catch (error) { + if (logDocgen) { + logger.error(error); + } + } + // inject __docgen prop in svelte component + const output = source + docgen; + + return output; +} diff --git a/addons/docs/src/frameworks/vue/config.ts b/addons/docs/src/frameworks/vue/config.ts new file mode 100644 index 000000000000..a5e41a726139 --- /dev/null +++ b/addons/docs/src/frameworks/vue/config.ts @@ -0,0 +1,15 @@ +import { extractArgTypes } from './extractArgTypes'; +import { extractComponentDescription } from '../../lib/docgen'; +import { prepareForInline } from './prepareForInline'; +import { sourceDecorator } from './sourceDecorator'; + +export const parameters = { + docs: { + inlineStories: true, + prepareForInline, + extractArgTypes, + extractComponentDescription, + }, +}; + +export const decorators = [sourceDecorator]; diff --git a/addons/docs/src/frameworks/vue/config.tsx b/addons/docs/src/frameworks/vue/config.tsx deleted file mode 100644 index f796827b56ba..000000000000 --- a/addons/docs/src/frameworks/vue/config.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import toReact from '@egoist/vue-to-react'; -import { StoryFn } from '@storybook/addons'; -import { addParameters } from '@storybook/client-api'; -import { extractArgTypes } from './extractArgTypes'; -import { extractComponentDescription } from '../../lib/docgen'; - -addParameters({ - docs: { - inlineStories: true, - prepareForInline: (storyFn: StoryFn) => { - const Story = toReact(storyFn()); - return ; - }, - extractArgTypes, - extractComponentDescription, - }, -}); diff --git a/addons/docs/src/frameworks/vue/extractArgTypes.ts b/addons/docs/src/frameworks/vue/extractArgTypes.ts index 8ee12409c1d2..fbd1f85fc205 100644 --- a/addons/docs/src/frameworks/vue/extractArgTypes.ts +++ b/addons/docs/src/frameworks/vue/extractArgTypes.ts @@ -1,12 +1,9 @@ import { ArgTypes } from '@storybook/api'; import { ArgTypesExtractor, hasDocgen, extractComponentProps } from '../../lib/docgen'; -import { convert } from '../../lib/sbtypes'; -import { trimQuotes } from '../../lib/sbtypes/utils'; +import { convert } from '../../lib/convert'; const SECTIONS = ['props', 'events', 'slots']; -const trim = (val: any) => (val && typeof val === 'string' ? trimQuotes(val) : val); - export const extractArgTypes: ArgTypesExtractor = (component) => { if (!hasDocgen(component)) { return null; @@ -15,17 +12,16 @@ export const extractArgTypes: ArgTypesExtractor = (component) => { SECTIONS.forEach((section) => { const props = extractComponentProps(component, section); props.forEach(({ propDef, docgenInfo, jsDocTags }) => { - const { name, type, description, defaultValue, required } = propDef; + const { name, type, description, defaultValue: defaultSummary, required } = propDef; const sbType = section === 'props' ? convert(docgenInfo) : { name: 'void' }; results[name] = { name, description, type: { required, ...sbType }, - defaultValue: defaultValue && trim(defaultValue.detail || defaultValue.summary), table: { type, jsDocTags, - defaultValue, + defaultValue: defaultSummary, category: section, }, }; diff --git a/addons/docs/src/frameworks/vue/prepareForInline.ts b/addons/docs/src/frameworks/vue/prepareForInline.ts new file mode 100644 index 000000000000..98931991e96b --- /dev/null +++ b/addons/docs/src/frameworks/vue/prepareForInline.ts @@ -0,0 +1,35 @@ +import React from 'react'; +import Vue from 'vue'; +import { StoryFn, StoryContext } from '@storybook/addons'; + +// Inspired by https://github.com/egoist/vue-to-react, +// modified to store args as props in the root store + +// FIXME get this from @storybook/vue +const COMPONENT = 'STORYBOOK_COMPONENT'; +const VALUES = 'STORYBOOK_VALUES'; + +export const prepareForInline = (storyFn: StoryFn, { args }: StoryContext) => { + const component = storyFn(); + const el = React.useRef(null); + + // FIXME: This recreates the Vue instance every time, which should be optimized + React.useEffect(() => { + const root = new Vue({ + el: el.current, + data() { + return { + [COMPONENT]: component, + [VALUES]: args, + }; + }, + render(h) { + const children = this[COMPONENT] ? [h(this[COMPONENT])] : undefined; + return h('div', { attrs: { id: 'root' } }, children); + }, + }); + return () => root.$destroy(); + }); + + return React.createElement('div', null, React.createElement('div', { ref: el })); +}; diff --git a/addons/docs/src/frameworks/vue/preset.ts b/addons/docs/src/frameworks/vue/preset.ts index f74526599697..5650c588cc67 100644 --- a/addons/docs/src/frameworks/vue/preset.ts +++ b/addons/docs/src/frameworks/vue/preset.ts @@ -1,12 +1,26 @@ -export function webpackFinal(webpackConfig: any = {}, options: any = {}) { +import type { Options } from '@storybook/core-common'; + +export function webpackFinal(webpackConfig: any = {}, options: Options) { + let vueDocgenOptions = {}; + + options.presetsList?.forEach((preset) => { + if (preset.name.includes('addon-docs') && preset.options.vueDocgenOptions) { + const appendableOptions = preset.options.vueDocgenOptions; + vueDocgenOptions = { + ...vueDocgenOptions, + ...appendableOptions, + }; + } + }); + webpackConfig.module.rules.push({ test: /\.vue$/, - loader: 'vue-docgen-loader', + loader: require.resolve('vue-docgen-loader', { paths: [require.resolve('@storybook/vue')] }), enforce: 'post', options: { docgenOptions: { alias: webpackConfig.resolve.alias, - ...options.vueDocgenOptions, + ...vueDocgenOptions, }, }, }); diff --git a/addons/docs/src/frameworks/vue/sourceDecorator.test.ts b/addons/docs/src/frameworks/vue/sourceDecorator.test.ts new file mode 100644 index 000000000000..54695c1ea84f --- /dev/null +++ b/addons/docs/src/frameworks/vue/sourceDecorator.test.ts @@ -0,0 +1,144 @@ +/* eslint no-underscore-dangle: ["error", { "allow": ["_vnode"] }] */ + +import { ComponentOptions } from 'vue'; +import Vue from 'vue/dist/vue'; +import { vnodeToString } from './sourceDecorator'; + +expect.addSnapshotSerializer({ + print: (val: any) => val, + test: (val) => typeof val === 'string', +}); + +const getVNode = (Component: ComponentOptions) => { + const vm = new Vue({ + render(h: (c: any) => unknown) { + return h(Component); + }, + }).$mount(); + + return vm.$children[0]._vnode; +}; + +describe('vnodeToString', () => { + it('basic', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('static class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('string dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('non-string dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('array dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('object dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('merge dynamic and static classes', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('attributes', () => { + const MyComponent: ComponentOptions = { + props: ['propA', 'propB', 'propC', 'propD', 'propE', 'propF', 'propG'], + template: '
', + }; + + expect( + vnodeToString( + getVNode({ + components: { MyComponent }, + data(): { props: Record } { + return { + props: { + propA: 'propA', + propB: 1, + propC: null, + propD: { + foo: 'bar', + }, + propE: true, + propF() { + const foo = 'bar'; + + return foo; + }, + propG: undefined, + }, + }; + }, + template: ``, + }) + ) + ).toMatchInlineSnapshot( + `` + ); + }); + + it('children', () => { + expect( + vnodeToString( + getVNode({ + template: ` +
+
+ +
+
`, + }) + ) + ).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/addons/docs/src/frameworks/vue/sourceDecorator.ts b/addons/docs/src/frameworks/vue/sourceDecorator.ts new file mode 100644 index 000000000000..7965dd6beff1 --- /dev/null +++ b/addons/docs/src/frameworks/vue/sourceDecorator.ts @@ -0,0 +1,234 @@ +/* eslint no-underscore-dangle: ["error", { "allow": ["_vnode"] }] */ + +import { addons, StoryContext } from '@storybook/addons'; +import { logger } from '@storybook/client-logger'; +import prettier from 'prettier/standalone'; +import prettierHtml from 'prettier/parser-html'; +import type Vue from 'vue'; + +import { SourceType, SNIPPET_RENDERED } from '../../shared'; + +export const skipSourceRender = (context: StoryContext) => { + const sourceParams = context?.parameters.docs?.source; + const isArgsStory = context?.parameters.__isArgsStory; + + // always render if the user forces it + if (sourceParams?.type === SourceType.DYNAMIC) { + return false; + } + + // never render if the user is forcing the block to render code, or + // if the user provides code, or if it's not an args story. + return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; +}; + +export const sourceDecorator = (storyFn: any, context: StoryContext) => { + const story = storyFn(); + + // See ../react/jsxDecorator.tsx + if (skipSourceRender(context)) { + return story; + } + + const channel = addons.getChannel(); + + const storyComponent = getStoryComponent(story.options.STORYBOOK_WRAPS); + + return { + components: { + Story: story, + }, + // We need to wait until the wrapper component to be mounted so Vue runtime + // struct VNode tree. We get `this._vnode == null` if switch to `created` + // lifecycle hook. + mounted() { + // Theoretically this does not happens but we need to check it. + if (!this._vnode) { + return; + } + + try { + const storyNode = lookupStoryInstance(this, storyComponent); + + const code = vnodeToString(storyNode._vnode); + + channel.emit( + SNIPPET_RENDERED, + (context || {}).id, + prettier.format(``, { + parser: 'vue', + plugins: [prettierHtml], + // Because the parsed vnode missing spaces right before/after the surround tag, + // we always get weird wrapped code without this option. + htmlWhitespaceSensitivity: 'ignore', + }) + ); + } catch (e) { + logger.warn(`Failed to generate dynamic story source: ${e}`); + } + }, + template: '', + }; +}; + +export function vnodeToString(vnode: Vue.VNode): string { + const attrString = [ + ...(vnode.data?.slot ? ([['slot', vnode.data.slot]] as [string, any][]) : []), + ['class', stringifyClassAttribute(vnode)], + ...(vnode.componentOptions?.propsData ? Object.entries(vnode.componentOptions.propsData) : []), + ...(vnode.data?.attrs ? Object.entries(vnode.data.attrs) : []), + ] + .filter(([name], index, list) => list.findIndex((item) => item[0] === name) === index) + .map(([name, value]) => stringifyAttr(name, value)) + .filter(Boolean) + .join(' '); + + if (!vnode.componentOptions) { + // Non-component elements (div, span, etc...) + if (vnode.tag) { + if (!vnode.children) { + return `<${vnode.tag} ${attrString}/>`; + } + + return `<${vnode.tag} ${attrString}>${vnode.children.map(vnodeToString).join('')}`; + } + + // TextNode + if (vnode.text) { + if (/[<>"&]/.test(vnode.text)) { + return `{{\`${vnode.text.replace(/`/g, '\\`')}\`}}`; + } + + return vnode.text; + } + + // Unknown + return ''; + } + + // Probably users never see the "unknown-component". It seems that vnode.tag + // is always set. + const tag = vnode.componentOptions.tag || vnode.tag || 'unknown-component'; + + if (!vnode.componentOptions.children) { + return `<${tag} ${attrString}/>`; + } + + return `<${tag} ${attrString}>${vnode.componentOptions.children + .map(vnodeToString) + .join('')}`; +} + +function stringifyClassAttribute(vnode: Vue.VNode): string | undefined { + if (!vnode.data || (!vnode.data.staticClass && !vnode.data.class)) { + return undefined; + } + + return ( + [...(vnode.data.staticClass?.split(' ') ?? []), ...normalizeClassBinding(vnode.data.class)] + .filter(Boolean) + .join(' ') || undefined + ); +} + +// https://vuejs.org/v2/guide/class-and-style.html#Binding-HTML-Classes +function normalizeClassBinding(binding: unknown): readonly string[] { + if (!binding) { + return []; + } + + if (typeof binding === 'string') { + return [binding]; + } + + if (binding instanceof Array) { + // To handle an object-in-array binding smartly, we use recursion + return binding.map(normalizeClassBinding).reduce((a, b) => [...a, ...b], []); + } + + if (typeof binding === 'object') { + return Object.entries(binding) + .filter(([, active]) => !!active) + .map(([className]) => className); + } + + // Unknown class binding + return []; +} + +function stringifyAttr(attrName: string, value?: any): string | null { + if (typeof value === 'undefined' || typeof value === 'function') { + return null; + } + + if (value === true) { + return attrName; + } + + if (typeof value === 'string') { + return `${attrName}=${quote(value)}`; + } + + // TODO: Better serialization (unquoted object key, Symbol/Classes, etc...) + // Seems like Prettier don't format JSON-look object (= when keys are quoted) + return `:${attrName}=${quote(JSON.stringify(value))}`; +} + +function quote(value: string) { + return value.includes(`"`) && !value.includes(`'`) + ? `'${value}'` + : `"${value.replace(/"/g, '"')}"`; +} + +/** + * Skip decorators and grab a story component itself. + * https://github.com/pocka/storybook-addon-vue-info/pull/113 + */ +function getStoryComponent(w: any) { + let matched = w; + + while ( + matched && + matched.options && + matched.options.components && + matched.options.components.story && + matched.options.components.story.options && + matched.options.components.story.options.STORYBOOK_WRAPS + ) { + matched = matched.options.components.story.options.STORYBOOK_WRAPS; + } + return matched; +} + +interface VueInternal { + // We need to access this private property, in order to grab the vnode of the + // component instead of the "vnode of the parent of the component". + // Probably it's safe to rely on this because vm.$vnode is a reference for this. + // https://github.com/vuejs/vue/issues/6070#issuecomment-314389883 + _vnode: Vue.VNode; +} + +/** + * Find the story's instance from VNode tree. + */ +function lookupStoryInstance(instance: Vue, storyComponent: any): (Vue & VueInternal) | null { + if ( + instance.$vnode && + instance.$vnode.componentOptions && + instance.$vnode.componentOptions.Ctor === storyComponent + ) { + return instance as Vue & VueInternal; + } + + for (let i = 0, l = instance.$children.length; i < l; i += 1) { + const found = lookupStoryInstance(instance.$children[i], storyComponent); + + if (found) { + return found; + } + } + + return null; +} diff --git a/addons/docs/src/frameworks/vue3/config.ts b/addons/docs/src/frameworks/vue3/config.ts new file mode 100644 index 000000000000..4a4fd37a39fb --- /dev/null +++ b/addons/docs/src/frameworks/vue3/config.ts @@ -0,0 +1,12 @@ +import { extractArgTypes } from './extractArgTypes'; +import { extractComponentDescription } from '../../lib/docgen'; +import { prepareForInline } from './prepareForInline'; + +export const parameters = { + docs: { + inlineStories: true, + prepareForInline, + extractArgTypes, + extractComponentDescription, + }, +}; diff --git a/addons/docs/src/frameworks/vue3/extractArgTypes.ts b/addons/docs/src/frameworks/vue3/extractArgTypes.ts new file mode 100644 index 000000000000..1555b6d6acc0 --- /dev/null +++ b/addons/docs/src/frameworks/vue3/extractArgTypes.ts @@ -0,0 +1,32 @@ +import { ArgTypes } from '@storybook/api'; +import { ArgTypesExtractor, hasDocgen, extractComponentProps } from '../../lib/docgen'; +import { convert } from '../../lib/convert'; + +const SECTIONS = ['props', 'events', 'slots']; + +export const extractArgTypes: ArgTypesExtractor = (component) => { + if (!hasDocgen(component)) { + return null; + } + const results: ArgTypes = {}; + SECTIONS.forEach((section) => { + const props = extractComponentProps(component, section); + props.forEach(({ propDef, docgenInfo, jsDocTags }) => { + const { name, type, description, defaultValue: defaultSummary, required } = propDef; + const sbType = section === 'props' ? convert(docgenInfo) : { name: 'void' }; + + results[name] = { + name, + description, + type: { required, ...sbType }, + table: { + type, + jsDocTags, + defaultValue: defaultSummary, + category: section, + }, + }; + }); + }); + return results; +}; diff --git a/addons/docs/src/frameworks/vue3/prepareForInline.ts b/addons/docs/src/frameworks/vue3/prepareForInline.ts new file mode 100644 index 000000000000..096dcf1349e6 --- /dev/null +++ b/addons/docs/src/frameworks/vue3/prepareForInline.ts @@ -0,0 +1,21 @@ +import React from 'react'; +import * as Vue from 'vue'; +import { StoryFn, StoryContext } from '@storybook/addons'; +import { app } from '@storybook/vue3'; + +// This is cast as `any` to workaround type errors caused by Vue 2 types +const { render, h } = Vue as any; + +export const prepareForInline = (storyFn: StoryFn, { args }: StoryContext) => { + const component = storyFn(); + + const vnode = h(component, args); + // By attaching the app context from `@storybook/vue3` to the vnode + // like this, these stoeis are able to access any app config stuff + // the end-user set inside `.storybook/preview.js` + vnode.appContext = app._context; // eslint-disable-line no-underscore-dangle + + return React.createElement('div', { + ref: (node?: HTMLDivElement): void => (node ? render(vnode, node) : null), + }); +}; diff --git a/addons/docs/src/frameworks/vue3/preset.ts b/addons/docs/src/frameworks/vue3/preset.ts new file mode 100644 index 000000000000..c8e044caf8e2 --- /dev/null +++ b/addons/docs/src/frameworks/vue3/preset.ts @@ -0,0 +1,28 @@ +import type { Options } from '@storybook/core-common'; + +export function webpackFinal(webpackConfig: any = {}, options: Options) { + let vueDocgenOptions = {}; + + options.presetsList?.forEach((preset) => { + if (preset.name.includes('addon-docs') && preset.options.vueDocgenOptions) { + const appendableOptions = preset.options.vueDocgenOptions; + vueDocgenOptions = { + ...vueDocgenOptions, + ...appendableOptions, + }; + } + }); + + webpackConfig.module.rules.push({ + test: /\.vue$/, + loader: require.resolve('vue-docgen-loader', { paths: [require.resolve('@storybook/vue3')] }), + enforce: 'post', + options: { + docgenOptions: { + alias: webpackConfig.resolve.alias, + ...vueDocgenOptions, + }, + }, + }); + return webpackConfig; +} diff --git a/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/custom-elements.snapshot b/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/custom-elements.snapshot index 162254124055..cbf4b8fa4254 100644 --- a/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/custom-elements.snapshot +++ b/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/custom-elements.snapshot @@ -24,6 +24,16 @@ Object { "type": "object", }, ], + "cssParts": Array [ + Object { + "description": "Front of the card", + "name": "front", + }, + Object { + "description": "Back of the card", + "name": "back", + }, + ], "cssProperties": Array [ Object { "description": "Header font size", diff --git a/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/input.js b/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/input.js index b56ff4c87b0a..64f03ae29da0 100644 --- a/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/input.js +++ b/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/input.js @@ -1,6 +1,8 @@ -import { CustomEvent } from 'global'; +import global from 'global'; import { LitElement, html, css } from 'lit-element'; +const { CustomEvent } = global; + const demoWcCardStyle = css` :host { display: block; @@ -105,6 +107,8 @@ const demoWcCardStyle = css` * @cssprop --demo-wc-card-header-font-size - Header font size * @cssprop --demo-wc-card-front-color - Font color for front * @cssprop --demo-wc-card-back-color - Font color for back + * @csspart front - Front of the card + * @csspart back - Back of the card */ export class DemoWcCard extends LitElement { static get properties() { @@ -148,10 +152,8 @@ export class DemoWcCard extends LitElement { render() { return html` -
-
- ${this.header} -
+
+
${this.header}
@@ -160,10 +162,8 @@ export class DemoWcCard extends LitElement {
-
-
- ${this.header} -
+
+
${this.header}
${this.rows.length === 0 diff --git a/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/properties.snapshot b/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/properties.snapshot index 39817364930f..57a2c58e8866 100644 --- a/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/properties.snapshot +++ b/addons/docs/src/frameworks/web-components/__testfixtures__/lit-element-demo-card/properties.snapshot @@ -24,7 +24,7 @@ Object { "name": "--demo-wc-card-back-color", "required": false, "table": Object { - "category": "css", + "category": "css custom properties", "defaultValue": Object { "summary": undefined, }, @@ -41,7 +41,7 @@ Object { "name": "--demo-wc-card-front-color", "required": false, "table": Object { - "category": "css", + "category": "css custom properties", "defaultValue": Object { "summary": undefined, }, @@ -58,7 +58,24 @@ Object { "name": "--demo-wc-card-header-font-size", "required": false, "table": Object { - "category": "css", + "category": "css custom properties", + "defaultValue": Object { + "summary": undefined, + }, + "type": Object { + "summary": undefined, + }, + }, + "type": Object { + "name": "void", + }, + }, + "back": Object { + "description": "Back of the card", + "name": "back", + "required": false, + "table": Object { + "category": "css shadow parts", "defaultValue": Object { "summary": undefined, }, @@ -104,6 +121,23 @@ Object { "name": "boolean", }, }, + "front": Object { + "description": "Front of the card", + "name": "front", + "required": false, + "table": Object { + "category": "css shadow parts", + "defaultValue": Object { + "summary": undefined, + }, + "type": Object { + "summary": undefined, + }, + }, + "type": Object { + "name": "void", + }, + }, "header": Object { "description": "Header message", "name": "header", diff --git a/addons/docs/src/frameworks/web-components/config.js b/addons/docs/src/frameworks/web-components/config.js index 18eec305f12e..b931d4e826d2 100644 --- a/addons/docs/src/frameworks/web-components/config.js +++ b/addons/docs/src/frameworks/web-components/config.js @@ -1,11 +1,9 @@ /* global window */ -/* eslint-disable import/no-extraneous-dependencies */ -import { addParameters } from '@storybook/client-api'; import React from 'react'; import { render } from 'lit-html'; import { extractArgTypes, extractComponentDescription } from './custom-elements'; -addParameters({ +export const parameters = { docs: { extractArgTypes, extractComponentDescription, @@ -25,8 +23,7 @@ addParameters({ return React.createElement('div', { ref: this.wrapperRef }); } } - return React.createElement(Story); }, }, -}); +}; diff --git a/addons/docs/src/frameworks/web-components/custom-elements.ts b/addons/docs/src/frameworks/web-components/custom-elements.ts index e46edcb7fdf6..701bfb866bac 100644 --- a/addons/docs/src/frameworks/web-components/custom-elements.ts +++ b/addons/docs/src/frameworks/web-components/custom-elements.ts @@ -1,12 +1,13 @@ -/* eslint-disable import/no-extraneous-dependencies */ import { getCustomElements, isValidComponent, isValidMetaData } from '@storybook/web-components'; import { ArgTypes } from '@storybook/api'; +import { logger } from '@storybook/client-logger'; interface TagItem { name: string; - type: string; + type: { text: string }; description: string; default?: any; + kind?: string; defaultValue?: any; } @@ -16,76 +17,124 @@ interface Tag { attributes?: TagItem[]; properties?: TagItem[]; events?: TagItem[]; + methods?: TagItem[]; + members?: TagItem[]; slots?: TagItem[]; cssProperties?: TagItem[]; + cssParts?: TagItem[]; } interface CustomElements { tags: Tag[]; + modules?: []; } +interface Module { + declarations?: []; + exports?: []; +} + +interface Declaration { + tagName: string; +} interface Sections { attributes?: any; properties?: any; events?: any; slots?: any; - css?: any; + cssCustomProperties?: any; + cssShadowParts?: any; } function mapData(data: TagItem[], category: string) { return ( data && - data.reduce((acc, item) => { - const type = category === 'properties' ? { name: item.type } : { name: 'void' }; - acc[item.name] = { - name: item.name, - required: false, - description: item.description, - type, - table: { - category, - type: { summary: item.type }, - defaultValue: { summary: item.default !== undefined ? item.default : item.defaultValue }, - }, - }; - return acc; - }, {} as ArgTypes) - ); -} + data + .filter((item) => !!item) + .reduce((acc, item) => { + if (item.kind === 'method') return acc; -function isEmpty(obj: object) { - return Object.entries(obj).length === 0 && obj.constructor === Object; + const type = + category === 'properties' ? { name: item.type?.text || item.type } : { name: 'void' }; + acc[item.name] = { + name: item.name, + required: false, + description: item.description, + type, + table: { + category, + type: { summary: item.type?.text || item.type }, + defaultValue: { + summary: item.default !== undefined ? item.default : item.defaultValue, + }, + }, + }; + return acc; + }, {} as ArgTypes) + ); } -export const extractArgTypesFromElements = (tagName: string, customElements: CustomElements) => { +const getMetaDataExperimental = (tagName: string, customElements: CustomElements) => { if (!isValidComponent(tagName) || !isValidMetaData(customElements)) { return null; } const metaData = customElements.tags.find( (tag) => tag.name.toUpperCase() === tagName.toUpperCase() ); - const argTypes = { - ...mapData(metaData.attributes, 'attributes'), - ...mapData(metaData.properties, 'properties'), - ...mapData(metaData.events, 'events'), - ...mapData(metaData.slots, 'slots'), - ...mapData(metaData.cssProperties, 'css'), - }; - return argTypes; -}; - -export const extractArgTypes = (tagName: string) => { - const customElements: CustomElements = getCustomElements(); - return extractArgTypesFromElements(tagName, customElements); + if (!metaData) { + logger.warn(`Component not found in custom-elements.json: ${tagName}`); + } + return metaData; }; -export const extractComponentDescription = (tagName: string) => { - const customElements: CustomElements = getCustomElements(); +const getMetaDataV1 = (tagName: string, customElements: CustomElements) => { if (!isValidComponent(tagName) || !isValidMetaData(customElements)) { return null; } - const metaData = customElements.tags.find( - (tag) => tag.name.toUpperCase() === tagName.toUpperCase() + + let metadata; + customElements?.modules?.forEach((_module: Module) => { + _module?.declarations?.forEach((declaration: Declaration) => { + if (declaration.tagName === tagName) { + metadata = declaration; + } + }); + }); + + if (!metadata) { + logger.warn(`Component not found in custom-elements.json: ${tagName}`); + } + return metadata; +}; + +export const extractArgTypesFromElements = (tagName: string, customElements: CustomElements) => { + const metaData = getMetaData(tagName, customElements); + return ( + metaData && { + ...mapData(metaData.attributes, 'attributes'), + ...mapData(metaData.members, 'properties'), + ...mapData(metaData.properties, 'properties'), + ...mapData(metaData.events, 'events'), + ...mapData(metaData.slots, 'slots'), + ...mapData(metaData.cssProperties, 'css custom properties'), + ...mapData(metaData.cssParts, 'css shadow parts'), + } ); +}; + +const getMetaData = (tagName: string, manifest: any) => { + if (manifest.version === 'experimental') { + return getMetaDataExperimental(tagName, manifest); + } + return getMetaDataV1(tagName, manifest); +}; + +export const extractArgTypes = (tagName: string) => { + const cem = getCustomElements(); + return extractArgTypesFromElements(tagName, cem); +}; + +export const extractComponentDescription = (tagName: string) => { + const metaData = getMetaData(tagName, getCustomElements()); return metaData && metaData.description; }; diff --git a/addons/docs/src/frameworks/web-components/web-components-properties.test.ts b/addons/docs/src/frameworks/web-components/web-components-properties.test.ts index 9f4581fdcb57..b4881bac3302 100644 --- a/addons/docs/src/frameworks/web-components/web-components-properties.test.ts +++ b/addons/docs/src/frameworks/web-components/web-components-properties.test.ts @@ -11,7 +11,7 @@ const inputRegExp = /^input\..*$/; const runWebComponentsAnalyzer = (inputPath: string) => { const { name: tmpDir, removeCallback } = tmp.dirSync(); const customElementsFile = `${tmpDir}/custom-elements.json`; - spawnSync('wca', ['analyze', inputPath, '--outFile', customElementsFile], { + spawnSync('yarn', ['wca', 'analyze', inputPath, '--outFile', customElementsFile], { stdio: 'inherit', }); const output = fs.readFileSync(customElementsFile, 'utf8'); @@ -28,6 +28,7 @@ describe('web-components component properties', () => { // because lit-html is distributed as ESM not CJS // https://github.com/Polymer/lit-html/issues/516 jest.mock('lit-html', () => {}); + jest.mock('lit-html/directive-helpers.js', () => {}); // eslint-disable-next-line global-require const { extractArgTypesFromElements } = require('./custom-elements'); @@ -37,6 +38,7 @@ describe('web-components component properties', () => { const testDir = path.join(fixturesDir, testEntry.name); const testFile = fs.readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); if (testFile) { + // eslint-disable-next-line jest/valid-title it(testEntry.name, () => { const inputPath = path.join(testDir, testFile); diff --git a/addons/docs/src/index.ts b/addons/docs/src/index.ts new file mode 100644 index 000000000000..91bf7d9c7408 --- /dev/null +++ b/addons/docs/src/index.ts @@ -0,0 +1 @@ +export * from './blocks'; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/arrays.js b/addons/docs/src/lib/convert/__testfixtures__/proptypes/arrays.js similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/arrays.js rename to addons/docs/src/lib/convert/__testfixtures__/proptypes/arrays.js diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/enums.js b/addons/docs/src/lib/convert/__testfixtures__/proptypes/enums.js similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/enums.js rename to addons/docs/src/lib/convert/__testfixtures__/proptypes/enums.js diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/misc.js b/addons/docs/src/lib/convert/__testfixtures__/proptypes/misc.js similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/misc.js rename to addons/docs/src/lib/convert/__testfixtures__/proptypes/misc.js diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/objects.js b/addons/docs/src/lib/convert/__testfixtures__/proptypes/objects.js similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/objects.js rename to addons/docs/src/lib/convert/__testfixtures__/proptypes/objects.js diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/react.js b/addons/docs/src/lib/convert/__testfixtures__/proptypes/react.js similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/react.js rename to addons/docs/src/lib/convert/__testfixtures__/proptypes/react.js diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/scalars.js b/addons/docs/src/lib/convert/__testfixtures__/proptypes/scalars.js similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/scalars.js rename to addons/docs/src/lib/convert/__testfixtures__/proptypes/scalars.js diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/aliases.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/aliases.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/aliases.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/aliases.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/arrays.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/arrays.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/arrays.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/arrays.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/enums.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/enums.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/enums.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/enums.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/functions.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/functions.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/functions.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/functions.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/interfaces.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/interfaces.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/interfaces.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/interfaces.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/intersections.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/intersections.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/intersections.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/intersections.tsx diff --git a/addons/docs/src/lib/convert/__testfixtures__/typescript/optionals.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/optionals.tsx new file mode 100644 index 000000000000..cced6cf2007c --- /dev/null +++ b/addons/docs/src/lib/convert/__testfixtures__/typescript/optionals.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; + +interface Props { + any?: any; + string?: string; + bool?: boolean; + number?: number; + symbol?: symbol; + readonly readonlyPrimitive?: string; +} +export const Component: FC = ({ + any = 'foo', + string = 'bar', + bool = true, + number = 4, + ...rest +}: Props) => { + const props = { any, string, bool, number, ...rest }; + return <>JSON.stringify(props); +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/records.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/records.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/records.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/records.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/scalars.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/scalars.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/scalars.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/scalars.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/tuples.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/tuples.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/tuples.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/tuples.tsx diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/unions.tsx b/addons/docs/src/lib/convert/__testfixtures__/typescript/unions.tsx similarity index 100% rename from addons/docs/src/lib/sbtypes/__testfixtures__/typescript/unions.tsx rename to addons/docs/src/lib/convert/__testfixtures__/typescript/unions.tsx diff --git a/addons/docs/src/lib/sbtypes/convert.test.ts b/addons/docs/src/lib/convert/convert.test.ts similarity index 99% rename from addons/docs/src/lib/sbtypes/convert.test.ts rename to addons/docs/src/lib/convert/convert.test.ts index 337586d82f4f..0cbdb3a324fd 100644 --- a/addons/docs/src/lib/sbtypes/convert.test.ts +++ b/addons/docs/src/lib/convert/convert.test.ts @@ -4,7 +4,7 @@ import { transformSync } from '@babel/core'; import requireFromString from 'require-from-string'; import fs from 'fs'; -import { convert } from './convert'; +import { convert } from './index'; import { normalizeNewlines } from '../utils'; expect.addSnapshotSerializer({ diff --git a/addons/docs/src/lib/convert/flow/convert.ts b/addons/docs/src/lib/convert/flow/convert.ts new file mode 100644 index 000000000000..b9ef15867ea7 --- /dev/null +++ b/addons/docs/src/lib/convert/flow/convert.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-case-declarations */ +import { SBType } from '@storybook/client-api'; +import { FlowType, FlowSigType, FlowLiteralType } from './types'; + +const isLiteral = (type: FlowType) => type.name === 'literal'; +const toEnumOption = (element: FlowLiteralType) => element.value.replace(/['|"]/g, ''); + +const convertSig = (type: FlowSigType) => { + switch (type.type) { + case 'function': + return { name: 'function' }; + case 'object': + const values: any = {}; + type.signature.properties.forEach((prop) => { + values[prop.key] = convert(prop.value); + }); + return { + name: 'object', + value: values, + }; + default: + throw new Error(`Unknown: ${type}`); + } +}; + +export const convert = (type: FlowType): SBType | void => { + const { name, raw } = type; + const base: any = {}; + if (typeof raw !== 'undefined') base.raw = raw; + switch (type.name) { + case 'literal': + return { ...base, name: 'other', value: type.value }; + case 'string': + case 'number': + case 'symbol': + case 'boolean': { + return { ...base, name }; + } + case 'Array': { + return { ...base, name: 'array', value: type.elements.map(convert) }; + } + case 'signature': + return { ...base, ...convertSig(type) }; + case 'union': + if (type.elements.every(isLiteral)) { + return { ...base, name: 'enum', value: type.elements.map(toEnumOption) }; + } + return { ...base, name, value: type.elements.map(convert) }; + + case 'intersection': + return { ...base, name, value: type.elements.map(convert) }; + default: + return { ...base, name: 'other', value: name }; + } +}; diff --git a/addons/docs/src/lib/sbtypes/index.ts b/addons/docs/src/lib/convert/flow/index.ts similarity index 100% rename from addons/docs/src/lib/sbtypes/index.ts rename to addons/docs/src/lib/convert/flow/index.ts diff --git a/addons/docs/src/lib/convert/flow/types.ts b/addons/docs/src/lib/convert/flow/types.ts new file mode 100644 index 000000000000..b131ac575526 --- /dev/null +++ b/addons/docs/src/lib/convert/flow/types.ts @@ -0,0 +1,56 @@ +interface FlowBaseType { + name: string; + type?: string; + raw?: string; + required?: boolean; +} + +type FlowArgType = FlowType; + +type FlowCombinationType = FlowBaseType & { + name: 'union' | 'intersection'; + elements: FlowType[]; +}; + +type FlowFuncSigType = FlowBaseType & { + name: 'signature'; + type: 'function'; + signature: { + arguments: FlowArgType[]; + return: FlowType; + }; +}; + +type FlowObjectSigType = FlowBaseType & { + name: 'signature'; + type: 'object'; + signature: { + properties: { + key: string; + value: FlowType; + }[]; + }; +}; + +type FlowScalarType = FlowBaseType & { + name: 'any' | 'boolean' | 'number' | 'void' | 'string' | 'symbol'; +}; + +export type FlowLiteralType = FlowBaseType & { + name: 'literal'; + value: string; +}; + +type FlowArrayType = FlowBaseType & { + name: 'Array'; + elements: FlowType[]; +}; + +export type FlowSigType = FlowObjectSigType | FlowFuncSigType; + +export type FlowType = + | FlowScalarType + | FlowLiteralType + | FlowCombinationType + | FlowSigType + | FlowArrayType; diff --git a/addons/docs/src/lib/convert/index.ts b/addons/docs/src/lib/convert/index.ts new file mode 100644 index 000000000000..f6cd38a77850 --- /dev/null +++ b/addons/docs/src/lib/convert/index.ts @@ -0,0 +1,13 @@ +import { DocgenInfo } from '../docgen/types'; +import { convert as tsConvert, TSType } from './typescript'; +import { convert as flowConvert, FlowType } from './flow'; +import { convert as propTypesConvert } from './proptypes'; + +export const convert = (docgenInfo: DocgenInfo) => { + const { type, tsType, flowType } = docgenInfo; + if (type != null) return propTypesConvert(type); + if (tsType != null) return tsConvert(tsType as TSType); + if (flowType != null) return flowConvert(flowType as FlowType); + + return null; +}; diff --git a/addons/docs/src/lib/convert/proptypes/convert.ts b/addons/docs/src/lib/convert/proptypes/convert.ts new file mode 100644 index 000000000000..bc4da9573401 --- /dev/null +++ b/addons/docs/src/lib/convert/proptypes/convert.ts @@ -0,0 +1,62 @@ +/* eslint-disable no-case-declarations */ +import mapValues from 'lodash/mapValues'; +import { SBType } from '@storybook/client-api'; +import { PTType } from './types'; +import { trimQuotes } from '../utils'; + +const SIGNATURE_REGEXP = /^\(.*\) => /; + +export const convert = (type: PTType): SBType | any => { + const { name, raw, computed, value } = type; + const base: any = {}; + if (typeof raw !== 'undefined') base.raw = raw; + + switch (name) { + case 'enum': { + const values = computed ? value : value.map((v: PTType) => trimQuotes(v.value)); + return { ...base, name, value: values }; + } + case 'string': + case 'number': + case 'symbol': + return { ...base, name }; + case 'func': + return { ...base, name: 'function' }; + case 'bool': + case 'boolean': + return { ...base, name: 'boolean' }; + case 'arrayOf': + case 'array': + return { ...base, name: 'array', value: value && convert(value as PTType) }; + case 'object': + return { ...base, name }; + case 'objectOf': + return { ...base, name, value: convert(value as PTType) }; + case 'shape': + case 'exact': + const values = mapValues(value, (field) => convert(field)); + return { ...base, name: 'object', value: values }; + case 'union': + return { ...base, name: 'union', value: value.map((v: PTType) => convert(v)) }; + case 'instanceOf': + case 'element': + case 'elementType': + default: { + if (name?.indexOf('|') > 0) { + // react-docgen-typescript-plugin doesn't always produce enum-like unions + // (like if a user has turned off shouldExtractValuesFromUnion) so here we + // try to recover and construct one. + try { + const literalValues = name.split('|').map((v: string) => JSON.parse(v)); + return { ...base, name: 'enum', value: literalValues }; + } catch (err) { + // fall through + } + } + const otherVal = value ? `${name}(${value})` : name; + const otherName = SIGNATURE_REGEXP.test(name) ? 'function' : 'other'; + + return { ...base, name: otherName, value: otherVal }; + } + } +}; diff --git a/addons/docs/src/lib/sbtypes/proptypes/index.ts b/addons/docs/src/lib/convert/proptypes/index.ts similarity index 100% rename from addons/docs/src/lib/sbtypes/proptypes/index.ts rename to addons/docs/src/lib/convert/proptypes/index.ts diff --git a/addons/docs/src/lib/sbtypes/proptypes/types.ts b/addons/docs/src/lib/convert/proptypes/types.ts similarity index 100% rename from addons/docs/src/lib/sbtypes/proptypes/types.ts rename to addons/docs/src/lib/convert/proptypes/types.ts diff --git a/addons/docs/src/lib/convert/typescript/convert.ts b/addons/docs/src/lib/convert/typescript/convert.ts new file mode 100644 index 000000000000..3affe3e70487 --- /dev/null +++ b/addons/docs/src/lib/convert/typescript/convert.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-case-declarations */ +import { SBType } from '@storybook/client-api'; +import { TSType, TSSigType } from './types'; + +const convertSig = (type: TSSigType) => { + switch (type.type) { + case 'function': + return { name: 'function' }; + case 'object': + const values: any = {}; + type.signature.properties.forEach((prop) => { + values[prop.key] = convert(prop.value); + }); + return { + name: 'object', + value: values, + }; + default: + throw new Error(`Unknown: ${type}`); + } +}; + +export const convert = (type: TSType): SBType | void => { + const { name, raw } = type; + const base: any = {}; + if (typeof raw !== 'undefined') base.raw = raw; + switch (type.name) { + case 'string': + case 'number': + case 'symbol': + case 'boolean': { + return { ...base, name }; + } + case 'Array': { + return { ...base, name: 'array', value: type.elements.map(convert) }; + } + case 'signature': + return { ...base, ...convertSig(type) }; + case 'union': + case 'intersection': + return { ...base, name, value: type.elements.map(convert) }; + default: + return { ...base, name: 'other', value: name }; + } +}; diff --git a/addons/docs/src/lib/sbtypes/typescript/index.ts b/addons/docs/src/lib/convert/typescript/index.ts similarity index 100% rename from addons/docs/src/lib/sbtypes/typescript/index.ts rename to addons/docs/src/lib/convert/typescript/index.ts diff --git a/addons/docs/src/lib/sbtypes/typescript/types.ts b/addons/docs/src/lib/convert/typescript/types.ts similarity index 100% rename from addons/docs/src/lib/sbtypes/typescript/types.ts rename to addons/docs/src/lib/convert/typescript/types.ts diff --git a/addons/docs/src/lib/sbtypes/utils.ts b/addons/docs/src/lib/convert/utils.ts similarity index 100% rename from addons/docs/src/lib/sbtypes/utils.ts rename to addons/docs/src/lib/convert/utils.ts diff --git a/lib/components/src/blocks/PropsTable/PropDef.ts b/addons/docs/src/lib/docgen/PropDef.ts similarity index 86% rename from lib/components/src/blocks/PropsTable/PropDef.ts rename to addons/docs/src/lib/docgen/PropDef.ts index d81a85334e20..b204085397b8 100644 --- a/lib/components/src/blocks/PropsTable/PropDef.ts +++ b/addons/docs/src/lib/docgen/PropDef.ts @@ -1,3 +1,5 @@ +// FIXME: this is legacy code that needs to be updated & simplified with ArgType refactor + export interface JsDocParam { name: string; description?: string; diff --git a/addons/docs/src/lib/docgen/createPropDef.ts b/addons/docs/src/lib/docgen/createPropDef.ts index 2fb88bd66d78..3916a3aeff35 100644 --- a/addons/docs/src/lib/docgen/createPropDef.ts +++ b/addons/docs/src/lib/docgen/createPropDef.ts @@ -1,11 +1,11 @@ -import { PropDef, PropDefaultValue } from '@storybook/components'; -import { TypeSystem, DocgenInfo, DocgenType, DocgenPropDefaultValue } from './types'; +import { PropDefaultValue } from '@storybook/components'; +import { PropDef, TypeSystem, DocgenInfo, DocgenType, DocgenPropDefaultValue } from './types'; import { JsDocParsingResult } from '../jsdocParser'; import { createSummaryValue } from '../utils'; import { createFlowPropDef } from './flow/createPropDef'; import { isDefaultValueBlacklisted } from './utils/defaultValue'; import { createTsPropDef } from './typeScript/createPropDef'; -import { convert } from '../sbtypes'; +import { convert } from '../convert'; export type PropDefFactory = ( propName: string, @@ -18,11 +18,46 @@ function createType(type: DocgenType) { return type != null ? createSummaryValue(type.name) : null; } -function createDefaultValue(defaultValue: DocgenPropDefaultValue): PropDefaultValue { +// A heuristic to tell if a defaultValue comes from RDT +function isReactDocgenTypescript(defaultValue: DocgenPropDefaultValue) { + const { computed, func } = defaultValue; + return typeof computed === 'undefined' && typeof func === 'undefined'; +} + +function isStringValued(type?: DocgenType) { + if (!type) { + return false; + } + + if (type.name === 'string') { + return true; + } + + if (type.name === 'enum') { + return ( + Array.isArray(type.value) && + type.value.every( + ({ value: tv }) => typeof tv === 'string' && tv[0] === '"' && tv[tv.length - 1] === '"' + ) + ); + } + return false; +} + +function createDefaultValue( + defaultValue: DocgenPropDefaultValue, + type: DocgenType +): PropDefaultValue { if (defaultValue != null) { const { value } = defaultValue; if (!isDefaultValueBlacklisted(value)) { + // Work around a bug in `react-docgen-typescript-loader`, which returns 'string' for a string + // default, instead of "'string'" -- which is incorrect + if (isReactDocgenTypescript(defaultValue) && isStringValued(type)) { + return createSummaryValue(JSON.stringify(value)); + } + return createSummaryValue(value); } } @@ -38,7 +73,7 @@ function createBasicPropDef(name: string, type: DocgenType, docgenInfo: DocgenIn type: createType(type), required, description, - defaultValue: createDefaultValue(defaultValue), + defaultValue: createDefaultValue(defaultValue, type), }; } diff --git a/addons/docs/src/lib/docgen/extractDocgenProps.test.ts b/addons/docs/src/lib/docgen/extractDocgenProps.test.ts index 921d329893f8..5195cf2e4b3a 100644 --- a/addons/docs/src/lib/docgen/extractDocgenProps.test.ts +++ b/addons/docs/src/lib/docgen/extractDocgenProps.test.ts @@ -60,7 +60,8 @@ TypeSystems.forEach((x) => { ...createStringType(x), description: 'Hey! Hey!', defaultValue: { - value: 'Default', + value: "'Default'", + computed: false, }, }); @@ -70,9 +71,69 @@ TypeSystems.forEach((x) => { expect(propDef.type.summary).toBe('string'); expect(propDef.description).toBe('Hey! Hey!'); expect(propDef.required).toBe(false); - expect(propDef.defaultValue.summary).toBe('Default'); + expect(propDef.defaultValue.summary).toBe("'Default'"); }); + if (x === TypeSystems[0]) { + // NOTE: `react-docgen-typescript currently doesn't serialize string as expected + it('should map defaults docgen info properly, RDT broken strings', () => { + const component = createComponent({ + ...createStringType(x), + description: 'Hey! Hey!', + defaultValue: { + value: 'Default', + }, + }); + + const { propDef } = extractComponentProps(component, DOCGEN_SECTION)[0]; + + expect(propDef.name).toBe(PROP_NAME); + expect(propDef.type.summary).toBe('string'); + expect(propDef.description).toBe('Hey! Hey!'); + expect(propDef.required).toBe(false); + expect(propDef.defaultValue.summary).toBe('"Default"'); + }); + + it('should map defaults docgen info properly, RDT broken enums', () => { + const component = createComponent({ + [x.typeProperty]: createType('enum', { + value: [{ value: '"Default"' }, { value: '"Other"' }], + }), + description: 'Hey! Hey!', + defaultValue: { + value: 'Default', + }, + }); + + const { propDef } = extractComponentProps(component, DOCGEN_SECTION)[0]; + + expect(propDef.name).toBe(PROP_NAME); + expect(propDef.type.summary).toBe('enum'); + expect(propDef.description).toBe('Hey! Hey!'); + expect(propDef.required).toBe(false); + expect(propDef.defaultValue.summary).toBe('"Default"'); + }); + + it('should map defaults docgen info properly, vue', () => { + const component = createComponent({ + ...createStringType(x), + description: 'Hey! Hey!', + defaultValue: { + value: "'Default'", + func: false, + }, + }); + + const { propDef } = extractComponentProps(component, DOCGEN_SECTION)[0]; + + expect(propDef.name).toBe(PROP_NAME); + expect(propDef.type.summary).toBe('string'); + expect(propDef.description).toBe('Hey! Hey!'); + expect(propDef.required).toBe(false); + expect(propDef.defaultValue.summary).toBe("'Default'"); + }); + } + it('should remove JSDoc tags from the description', () => { const component = createComponent({ ...createStringType(x), diff --git a/addons/docs/src/lib/docgen/extractDocgenProps.ts b/addons/docs/src/lib/docgen/extractDocgenProps.ts index e66468b02b90..c7a9090adf4f 100644 --- a/addons/docs/src/lib/docgen/extractDocgenProps.ts +++ b/addons/docs/src/lib/docgen/extractDocgenProps.ts @@ -1,7 +1,6 @@ -import { PropDef } from '@storybook/components'; import { Component } from '../../blocks/types'; import { ExtractedJsDoc, parseJsDoc } from '../jsdocParser'; -import { DocgenInfo, TypeSystem } from './types'; +import { PropDef, DocgenInfo, TypeSystem } from './types'; import { getDocgenSection, isValidDocgenSection, getDocgenDescription } from './utils'; import { getPropDefFactory, PropDefFactory } from './createPropDef'; @@ -34,9 +33,19 @@ export const extractComponentSectionArray = (docgenSection: any) => { const typeSystem = getTypeSystem(docgenSection[0]); const createPropDef = getPropDefFactory(typeSystem); - return docgenSection - .map((item: any) => extractProp(item.name, item, typeSystem, createPropDef)) - .filter(Boolean); + return docgenSection.map((item: any) => { + let sanitizedItem = item; + if (item.type?.elements) { + sanitizedItem = { + ...item, + type: { + ...item.type, + value: item.type.elements, + }, + }; + } + return extractProp(sanitizedItem.name, sanitizedItem, typeSystem, createPropDef); + }); }; export const extractComponentSectionObject = (docgenSection: any) => { diff --git a/addons/docs/src/lib/docgen/flow/createPropDef.test.ts b/addons/docs/src/lib/docgen/flow/createPropDef.test.ts index be904ce7af72..4b563e3f86db 100644 --- a/addons/docs/src/lib/docgen/flow/createPropDef.test.ts +++ b/addons/docs/src/lib/docgen/flow/createPropDef.test.ts @@ -269,6 +269,80 @@ describe('type', () => { expect(type.summary).toBe('number | string'); }); + it('should support nested union elements', () => { + const docgenInfo = createDocgenInfo({ + flowType: { + name: 'union', + raw: '"minimum" | "maximum" | UserSize', + elements: [ + { + name: 'literal', + value: '"minimum"', + }, + { + name: 'literal', + value: '"maximum"', + }, + { + name: 'union', + raw: 'string | number', + elements: [ + { + name: 'number', + }, + { + name: 'string', + }, + ], + }, + ], + }, + }); + + const { type } = createFlowPropDef(PROP_NAME, docgenInfo); + + expect(type.summary).toBe('"minimum" | "maximum" | number | string'); + }); + + it('uses raw union value if elements are missing', () => { + const docgenInfo = createDocgenInfo({ + flowType: { + name: 'union', + raw: '"minimum" | "maximum" | UserSize', + }, + }); + + const { type } = createFlowPropDef(PROP_NAME, docgenInfo); + + expect(type.summary).toBe('"minimum" | "maximum" | UserSize'); + }); + + it('removes a leading | if raw union value is used', () => { + const docgenInfo = createDocgenInfo({ + flowType: { + name: 'union', + raw: '| "minimum" | "maximum" | UserSize', + }, + }); + + const { type } = createFlowPropDef(PROP_NAME, docgenInfo); + + expect(type.summary).toBe('"minimum" | "maximum" | UserSize'); + }); + + it('even removes extra spaces after a leading | if raw union value is used', () => { + const docgenInfo = createDocgenInfo({ + flowType: { + name: 'union', + raw: '| "minimum" | "maximum" | UserSize', + }, + }); + + const { type } = createFlowPropDef(PROP_NAME, docgenInfo); + + expect(type.summary).toBe('"minimum" | "maximum" | UserSize'); + }); + it('should support intersection', () => { const docgenInfo = createDocgenInfo({ flowType: { diff --git a/addons/docs/src/lib/docgen/flow/createType.ts b/addons/docs/src/lib/docgen/flow/createType.ts index 5cf193fb53bf..2e9d86a93a4f 100644 --- a/addons/docs/src/lib/docgen/flow/createType.ts +++ b/addons/docs/src/lib/docgen/flow/createType.ts @@ -7,17 +7,41 @@ enum FlowTypesType { SIGNATURE = 'signature', } +interface DocgenFlowUnionElement { + name: string; + value?: string; + elements?: DocgenFlowUnionElement[]; + raw?: string; +} + interface DocgenFlowUnionType extends DocgenFlowType { - elements: { name: string; value: string }[]; + elements: DocgenFlowUnionElement[]; } -function generateUnion({ name, raw, elements }: DocgenFlowUnionType): PropType { +function generateUnionElement({ name, value, elements, raw }: DocgenFlowUnionElement): string { + if (value != null) { + return value; + } + + if (elements != null) { + return elements.map(generateUnionElement).join(' | '); + } + if (raw != null) { - return createSummaryValue(raw); + return raw; } + return name; +} + +function generateUnion({ name, raw, elements }: DocgenFlowUnionType): PropType { if (elements != null) { - return createSummaryValue(elements.map((x) => x.value).join(' | ')); + return createSummaryValue(elements.map(generateUnionElement).join(' | ')); + } + + if (raw != null) { + // Flow Unions can be defined with or without a leading `|` character, so try to remove it. + return createSummaryValue(raw.replace(/^\|\s*/, '')); } return createSummaryValue(name); diff --git a/addons/docs/src/lib/docgen/types.ts b/addons/docs/src/lib/docgen/types.ts index f95a54e84641..fcae772fa5b9 100644 --- a/addons/docs/src/lib/docgen/types.ts +++ b/addons/docs/src/lib/docgen/types.ts @@ -1,8 +1,8 @@ -import { PropsTableProps } from '@storybook/components'; import { ArgTypes } from '@storybook/api'; +import { PropDef } from './PropDef'; import { Component } from '../../blocks/types'; -export type PropsExtractor = (component: Component) => PropsTableProps | null; +export type PropsExtractor = (component: Component) => { rows?: PropDef[] } | null; export type ArgTypesExtractor = (component: Component) => ArgTypes | null; @@ -10,6 +10,7 @@ export interface DocgenType { name: string; description?: string; required?: boolean; + value?: any; // Seems like this can be many things } export interface DocgenPropType extends DocgenType { @@ -32,6 +33,8 @@ export interface DocgenTypeScriptType extends DocgenType {} export interface DocgenPropDefaultValue { value: string; + computed?: boolean; + func?: boolean; } export interface DocgenInfo { @@ -49,3 +52,5 @@ export enum TypeSystem { TYPESCRIPT = 'TypeScript', UNKNOWN = 'Unknown', } + +export type { PropDef }; diff --git a/addons/docs/src/lib/sbtypes/convert.ts b/addons/docs/src/lib/sbtypes/convert.ts deleted file mode 100644 index 9b7b0fb4df27..000000000000 --- a/addons/docs/src/lib/sbtypes/convert.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DocgenInfo } from '../docgen/types'; -import { convert as tsConvert, TSType } from './typescript'; -import { convert as propTypesConvert } from './proptypes'; - -export const convert = (docgenInfo: DocgenInfo) => { - const { type, tsType } = docgenInfo; - if (type != null) return propTypesConvert(type); - if (tsType != null) return tsConvert(tsType as TSType); - - return null; -}; diff --git a/addons/docs/src/lib/sbtypes/proptypes/convert.ts b/addons/docs/src/lib/sbtypes/proptypes/convert.ts deleted file mode 100644 index 105e18477826..000000000000 --- a/addons/docs/src/lib/sbtypes/proptypes/convert.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable no-case-declarations */ -import mapValues from 'lodash/mapValues'; -import { PTType } from './types'; -import { SBType } from '../types'; -import { trimQuotes } from '../utils'; - -const SIGNATURE_REGEXP = /^\(.*\) => /; - -export const convert = (type: PTType): SBType | any => { - const { name, raw, computed, value } = type; - const base: any = {}; - if (typeof raw !== 'undefined') base.raw = raw; - - switch (name) { - case 'enum': { - const values = computed ? value : value.map((v: PTType) => trimQuotes(v.value)); - return { ...base, name, value: values }; - } - case 'string': - case 'number': - case 'symbol': - return { ...base, name }; - case 'func': - return { ...base, name: 'function' }; - case 'bool': - case 'boolean': - return { ...base, name: 'boolean' }; - case 'arrayOf': - case 'array': - return { ...base, name: 'array', value: value && convert(value as PTType) }; - case 'object': - return { ...base, name }; - case 'objectOf': - return { ...base, name, value: convert(value as PTType) }; - case 'shape': - case 'exact': - const values = mapValues(value, (field) => convert(field)); - return { ...base, name: 'object', value: values }; - case 'union': - return { ...base, name: 'union', value: value.map((v: PTType) => convert(v)) }; - case 'instanceOf': - case 'element': - case 'elementType': - default: - const otherVal = value ? `${name}(${value})` : name; - const otherName = SIGNATURE_REGEXP.test(name) ? 'function' : 'other'; - return { ...base, name: otherName, value: otherVal }; - } -}; diff --git a/addons/docs/src/lib/sbtypes/types.ts b/addons/docs/src/lib/sbtypes/types.ts deleted file mode 100644 index d0fbed123f3f..000000000000 --- a/addons/docs/src/lib/sbtypes/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -interface SBBaseType { - required?: boolean; - raw?: string; -} - -export type SBScalarType = SBBaseType & { - name: 'boolean' | 'string' | 'number' | 'function'; -}; - -export type SBArrayType = SBBaseType & { - name: 'array'; - value: SBType; -}; -export type SBObjectType = SBBaseType & { - name: 'object'; - value: Record; -}; -export type SBEnumType = SBBaseType & { - name: 'enum'; - value: (string | number)[]; -}; -export type SBIntersectionType = SBBaseType & { - name: 'intersection'; - value: SBType[]; -}; -export type SBUnionType = SBBaseType & { - name: 'union'; - value: SBType[]; -}; -export type SBOtherType = SBBaseType & { - name: 'other'; - value: string; -}; - -export type SBType = - | SBScalarType - | SBEnumType - | SBArrayType - | SBObjectType - | SBIntersectionType - | SBUnionType - | SBOtherType; diff --git a/addons/docs/src/lib/sbtypes/typescript/convert.ts b/addons/docs/src/lib/sbtypes/typescript/convert.ts deleted file mode 100644 index a53d0d79cbce..000000000000 --- a/addons/docs/src/lib/sbtypes/typescript/convert.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-case-declarations */ -import { TSType, TSSigType } from './types'; -import { SBType } from '../types'; - -const convertSig = (type: TSSigType) => { - switch (type.type) { - case 'function': - return { name: 'function' }; - case 'object': - const values: any = {}; - type.signature.properties.forEach((prop) => { - values[prop.key] = convert(prop.value); - }); - return { - name: 'object', - value: values, - }; - default: - throw new Error(`Unknown: ${type}`); - } -}; - -export const convert = (type: TSType): SBType | void => { - const { name, raw } = type; - const base: any = {}; - if (typeof raw !== 'undefined') base.raw = raw; - switch (type.name) { - case 'string': - case 'number': - case 'symbol': - case 'boolean': { - return { ...base, name }; - } - case 'Array': { - return { ...base, name: 'array', value: type.elements.map(convert) }; - } - case 'signature': - return { ...base, ...convertSig(type) }; - case 'union': - case 'intersection': - return { ...base, name, value: type.elements.map(convert) }; - default: - return { ...base, name: 'other', value: name }; - } -}; diff --git a/addons/docs/src/lib/utils.test.ts b/addons/docs/src/lib/utils.test.ts new file mode 100644 index 000000000000..0cd887200ded --- /dev/null +++ b/addons/docs/src/lib/utils.test.ts @@ -0,0 +1,20 @@ +import { createSummaryValue } from './utils'; + +describe('createSummaryValue', () => { + it('creates an object with just summary if detail is not passed', () => { + const summary = 'boolean'; + expect(createSummaryValue(summary)).toEqual({ summary }); + }); + + it('creates an object with summary & detail if passed', () => { + const summary = 'MyType'; + const detail = 'boolean | string'; + expect(createSummaryValue(summary, detail)).toEqual({ summary, detail }); + }); + + it('creates an object with just summary if details are equal', () => { + const summary = 'boolean'; + const detail = 'boolean'; + expect(createSummaryValue(summary, detail)).toEqual({ summary }); + }); +}); diff --git a/addons/docs/src/lib/utils.ts b/addons/docs/src/lib/utils.ts index 44725a086956..b623c12e19b7 100644 --- a/addons/docs/src/lib/utils.ts +++ b/addons/docs/src/lib/utils.ts @@ -1,17 +1,20 @@ import { PropSummaryValue } from '@storybook/components'; export const MAX_TYPE_SUMMARY_LENGTH = 90; -export const MAX_DEFALUT_VALUE_SUMMARY_LENGTH = 50; +export const MAX_DEFAULT_VALUE_SUMMARY_LENGTH = 50; export function isTooLongForTypeSummary(value: string): boolean { return value.length > MAX_TYPE_SUMMARY_LENGTH; } export function isTooLongForDefaultValueSummary(value: string): boolean { - return value.length > MAX_DEFALUT_VALUE_SUMMARY_LENGTH; + return value.length > MAX_DEFAULT_VALUE_SUMMARY_LENGTH; } export function createSummaryValue(summary: string, detail?: string): PropSummaryValue { + if (summary === detail) { + return { summary }; + } return { summary, detail }; } diff --git a/addons/docs/src/mdx/__testfixtures__/component-args.mdx b/addons/docs/src/mdx/__testfixtures__/component-args.mdx deleted file mode 100644 index d9a1c2d8e8b9..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/component-args.mdx +++ /dev/null @@ -1,10 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Args - - - - diff --git a/addons/docs/src/mdx/__testfixtures__/component-id.mdx b/addons/docs/src/mdx/__testfixtures__/component-id.mdx deleted file mode 100644 index e6ca2de9dadb..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/component-id.mdx +++ /dev/null @@ -1,8 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - - - - diff --git a/addons/docs/src/mdx/__testfixtures__/decorators.mdx b/addons/docs/src/mdx/__testfixtures__/decorators.mdx deleted file mode 100644 index 1a38b0053aef..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/decorators.mdx +++ /dev/null @@ -1,13 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - -
{storyFn()}
]} -/> - -# Decorated story - -
{storyFn()}
]}> - -
diff --git a/addons/docs/src/mdx/__testfixtures__/docs-only.mdx b/addons/docs/src/mdx/__testfixtures__/docs-only.mdx deleted file mode 100644 index 308dde0e8faf..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/docs-only.mdx +++ /dev/null @@ -1,7 +0,0 @@ -import { Meta } from '@storybook/addon-docs/blocks'; - - - -# Documentation only - -This is a documentation-only MDX file which generates a dummy `docsOnly: true` story. diff --git a/addons/docs/src/mdx/__testfixtures__/docs-only.output.snapshot b/addons/docs/src/mdx/__testfixtures__/docs-only.output.snapshot deleted file mode 100644 index 9b920702c186..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/docs-only.output.snapshot +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin docs-only.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Meta } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - -

{\`Documentation only\`}

-

- {\`This is a documentation-only MDX file which generates a dummy \`} - {\`docsOnly: true\`} - {\` story.\`} -

-
- ); -} - -MDXContent.isMDXComponent = true; - -export const __page = () => { - throw new Error('Docs-only story'); -}; - -__page.parameters = { docsOnly: true }; - -const componentMeta = { title: 'docs-only', includeStories: ['__page'] }; - -const mdxStoryNameToKey = {}; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/meta-quotes-in-title.mdx b/addons/docs/src/mdx/__testfixtures__/meta-quotes-in-title.mdx deleted file mode 100644 index 83705fe69282..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/meta-quotes-in-title.mdx +++ /dev/null @@ -1,5 +0,0 @@ -import { Meta } from '@storybook/addon-docs/blocks'; - - \ No newline at end of file diff --git a/addons/docs/src/mdx/__testfixtures__/meta-quotes-in-title.output.snapshot b/addons/docs/src/mdx/__testfixtures__/meta-quotes-in-title.output.snapshot deleted file mode 100644 index 48f6b3857e47..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/meta-quotes-in-title.output.snapshot +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin meta-quotes-in-title.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Meta } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - - - ); -} - -MDXContent.isMDXComponent = true; - -export const __page = () => { - throw new Error('Docs-only story'); -}; - -__page.parameters = { docsOnly: true }; - -const componentMeta = { title: \\"Addons/Docs/what's in a title?\\", includeStories: ['__page'] }; - -const mdxStoryNameToKey = {}; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/non-story-exports.mdx b/addons/docs/src/mdx/__testfixtures__/non-story-exports.mdx deleted file mode 100644 index 833dd03c1386..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/non-story-exports.mdx +++ /dev/null @@ -1,16 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Story definition - - - - - -export const two = 2; - - - - diff --git a/addons/docs/src/mdx/__testfixtures__/parameters.mdx b/addons/docs/src/mdx/__testfixtures__/parameters.mdx deleted file mode 100644 index 2b2e1c6b5546..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/parameters.mdx +++ /dev/null @@ -1,12 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - - - - - - - - diff --git a/addons/docs/src/mdx/__testfixtures__/previews.mdx b/addons/docs/src/mdx/__testfixtures__/previews.mdx deleted file mode 100644 index 8ec8874d102f..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/previews.mdx +++ /dev/null @@ -1,26 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Preview, Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Preview - -Previews can contain normal components, stories, and story references - - - - - - - - - - - - - -Preview wthout a story - - - - \ No newline at end of file diff --git a/addons/docs/src/mdx/__testfixtures__/previews.output.snapshot b/addons/docs/src/mdx/__testfixtures__/previews.output.snapshot deleted file mode 100644 index 17b6711fa28d..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/previews.output.snapshot +++ /dev/null @@ -1,86 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin previews.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Button } from '@storybook/react/demo'; -import { Preview, Story, Meta } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - -

{\`Preview\`}

-

{\`Previews can contain normal components, stories, and story references\`}

- - - - - - - - - - -

{\`Preview wthout a story\`}

- - - -
- ); -} - -MDXContent.isMDXComponent = true; - -export const helloButton = () => ; -helloButton.storyName = 'hello button'; -helloButton.parameters = { storySource: { source: '' } }; - -export const two = () => ; -two.storyName = 'two'; -two.parameters = { storySource: { source: '' } }; - -const componentMeta = { - title: 'Button', - parameters: { - notes: 'component notes', - }, - component: Button, - includeStories: ['helloButton', 'two'], -}; - -const mdxStoryNameToKey = { 'hello button': 'helloButton', two: 'two' }; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/story-args.mdx b/addons/docs/src/mdx/__testfixtures__/story-args.mdx deleted file mode 100644 index 57a20578d9ec..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-args.mdx +++ /dev/null @@ -1,10 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Args - - - - diff --git a/addons/docs/src/mdx/__testfixtures__/story-args.output.snapshot b/addons/docs/src/mdx/__testfixtures__/story-args.output.snapshot deleted file mode 100644 index ef73decd1034..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-args.output.snapshot +++ /dev/null @@ -1,83 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin story-args.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - -

{\`Args\`}

- - - -
- ); -} - -MDXContent.isMDXComponent = true; - -export const componentNotes = () => ; -componentNotes.storyName = 'component notes'; -componentNotes.argTypes = { - a: { - name: 'A', - }, - b: { - name: 'B', - }, -}; -componentNotes.args = { - a: 1, - b: 2, -}; -componentNotes.parameters = { storySource: { source: '' } }; - -const componentMeta = { title: 'Button', includeStories: ['componentNotes'] }; - -const mdxStoryNameToKey = { 'component notes': 'componentNotes' }; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/story-current.mdx b/addons/docs/src/mdx/__testfixtures__/story-current.mdx deleted file mode 100644 index d5b62cc6edab..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-current.mdx +++ /dev/null @@ -1,5 +0,0 @@ -import { Story } from '@storybook/addon-docs/blocks'; - -# Current story - - diff --git a/addons/docs/src/mdx/__testfixtures__/story-current.output.snapshot b/addons/docs/src/mdx/__testfixtures__/story-current.output.snapshot deleted file mode 100644 index 870d3523fb66..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-current.output.snapshot +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin story-current.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Story } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - -

{\`Current story\`}

- -
- ); -} - -MDXContent.isMDXComponent = true; - -const componentMeta = { includeStories: [] }; - -const mdxStoryNameToKey = {}; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/story-def-text-only.mdx b/addons/docs/src/mdx/__testfixtures__/story-def-text-only.mdx deleted file mode 100644 index 78ebd50215ee..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-def-text-only.mdx +++ /dev/null @@ -1,7 +0,0 @@ -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Story definition - -Plain text diff --git a/addons/docs/src/mdx/__testfixtures__/story-def-text-only.output.snapshot b/addons/docs/src/mdx/__testfixtures__/story-def-text-only.output.snapshot deleted file mode 100644 index b8029e35582b..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-def-text-only.output.snapshot +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin story-def-text-only.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Story, Meta } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - -

{\`Story definition\`}

- - Plain text - -
- ); -} - -MDXContent.isMDXComponent = true; - -export const text = () => 'Plain text'; -text.storyName = 'text'; -text.parameters = { storySource: { source: \\"'Plain text'\\" } }; - -const componentMeta = { title: 'Text', includeStories: ['text'] }; - -const mdxStoryNameToKey = { text: 'text' }; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/story-definitions.mdx b/addons/docs/src/mdx/__testfixtures__/story-definitions.mdx deleted file mode 100644 index b50bb0df7e2c..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-definitions.mdx +++ /dev/null @@ -1,22 +0,0 @@ -import { Button } from '@storybook/react/demo'; -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Story definition - - - - - - - - - - - - - - - - diff --git a/addons/docs/src/mdx/__testfixtures__/story-function-var.mdx b/addons/docs/src/mdx/__testfixtures__/story-function-var.mdx deleted file mode 100644 index 7cfbae252f7b..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-function-var.mdx +++ /dev/null @@ -1,11 +0,0 @@ -import { Meta, Story } from '@storybook/addon-docs/blocks'; - - - -export const basicFn = () => - diff --git a/addons/docs/src/mdx/__testfixtures__/story-multiple-children.mdx b/addons/docs/src/mdx/__testfixtures__/story-multiple-children.mdx deleted file mode 100644 index 02e3ebed2453..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-multiple-children.mdx +++ /dev/null @@ -1,10 +0,0 @@ -import { Story, Meta } from '@storybook/addon-docs/blocks'; - - - -# Multiple children - - -

Hello Child #1

-

Hello Child #2

-
diff --git a/addons/docs/src/mdx/__testfixtures__/story-object.mdx b/addons/docs/src/mdx/__testfixtures__/story-object.mdx deleted file mode 100644 index e63d48923842..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-object.mdx +++ /dev/null @@ -1,19 +0,0 @@ -import { Story, Meta } from '@storybook/addon-docs/blocks'; -import { Welcome, Button } from '@storybook/angular/demo'; -import { linkTo } from '@storybook/addon-links'; - - - -# Story object - - - {{ - template: ``, - props: { - showApp: linkTo('Button'), - }, - moduleMetadata: { - declarations: [Welcome], - }, - }} - diff --git a/addons/docs/src/mdx/__testfixtures__/story-object.output.snapshot b/addons/docs/src/mdx/__testfixtures__/story-object.output.snapshot deleted file mode 100644 index aab6752d89d0..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-object.output.snapshot +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin story-object.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Story, Meta } from '@storybook/addon-docs/blocks'; -import { Welcome, Button } from '@storybook/angular/demo'; -import { linkTo } from '@storybook/addon-links'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - -

{\`Story object\`}

- - {{ - template: \`\`, - props: { - showApp: linkTo('Button'), - }, - moduleMetadata: { - declarations: [Welcome], - }, - }} - -
- ); -} - -MDXContent.isMDXComponent = true; - -export const toStorybook = () => ({ - template: \`\`, - props: { - showApp: linkTo('Button'), - }, - moduleMetadata: { - declarations: [Welcome], - }, -}); -toStorybook.storyName = 'to storybook'; -toStorybook.parameters = { - storySource: { - source: - '{\\\\n template: \`\`,\\\\n props: {\\\\n showApp: linkTo(\\\\'Button\\\\')\\\\n },\\\\n moduleMetadata: {\\\\n declarations: [Welcome]\\\\n }\\\\n}', - }, -}; - -const componentMeta = { title: 'MDX|Welcome', includeStories: ['toStorybook'] }; - -const mdxStoryNameToKey = { 'to storybook': 'toStorybook' }; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/story-references.mdx b/addons/docs/src/mdx/__testfixtures__/story-references.mdx deleted file mode 100644 index 865bd5dea49b..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-references.mdx +++ /dev/null @@ -1,5 +0,0 @@ -import { Story } from '@storybook/addon-docs/blocks'; - -# Story reference - - diff --git a/addons/docs/src/mdx/__testfixtures__/story-references.output.snapshot b/addons/docs/src/mdx/__testfixtures__/story-references.output.snapshot deleted file mode 100644 index 831e749ecaa7..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/story-references.output.snapshot +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin story-references.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Story } from '@storybook/addon-docs/blocks'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - -

{\`Story reference\`}

- -
- ); -} - -MDXContent.isMDXComponent = true; - -const componentMeta = { includeStories: [] }; - -const mdxStoryNameToKey = {}; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/title-template-string.mdx b/addons/docs/src/mdx/__testfixtures__/title-template-string.mdx deleted file mode 100644 index 3e41ab169fb5..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/title-template-string.mdx +++ /dev/null @@ -1,4 +0,0 @@ -import { Meta, Story } from '@storybook/addon-docs/blocks'; -import { titleFunction } from '../title-generators'; - - \ No newline at end of file diff --git a/addons/docs/src/mdx/__testfixtures__/title-template-string.output.snapshot b/addons/docs/src/mdx/__testfixtures__/title-template-string.output.snapshot deleted file mode 100644 index bf2d68da5617..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/title-template-string.output.snapshot +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin title-template-string.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Meta, Story } from '@storybook/addon-docs/blocks'; -import { titleFunction } from '../title-generators'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - - - - ); -} - -MDXContent.isMDXComponent = true; - -export const __page = () => { - throw new Error('Docs-only story'); -}; - -__page.parameters = { docsOnly: true }; - -const componentMeta = { title: \`\${titleFunction('template')}\`, includeStories: ['__page'] }; - -const mdxStoryNameToKey = {}; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/__testfixtures__/vanilla.mdx b/addons/docs/src/mdx/__testfixtures__/vanilla.mdx deleted file mode 100644 index 93ccac3d6bc3..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/vanilla.mdx +++ /dev/null @@ -1,7 +0,0 @@ -import { Button } from '@storybook/react/demo'; - -# Hello MDX - -This is some random content. - - diff --git a/addons/docs/src/mdx/__testfixtures__/vanilla.output.snapshot b/addons/docs/src/mdx/__testfixtures__/vanilla.output.snapshot deleted file mode 100644 index b9b591c2c0c0..000000000000 --- a/addons/docs/src/mdx/__testfixtures__/vanilla.output.snapshot +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`docs-mdx-compiler-plugin vanilla.mdx 1`] = ` -"/* @jsx mdx */ -import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks'; - -import { Button } from '@storybook/react/demo'; - -const makeShortcode = (name) => - function MDXDefaultShortcode(props) { - console.warn( - 'Component ' + - name + - ' was not imported, exported, or provided by MDXProvider as global scope' - ); - return
; - }; - -const layoutProps = {}; -const MDXLayout = 'wrapper'; -function MDXContent({ components, ...props }) { - return ( - -

{\`Hello MDX\`}

-

{\`This is some random content.\`}

- -
- ); -} - -MDXContent.isMDXComponent = true; - -const componentMeta = { includeStories: [] }; - -const mdxStoryNameToKey = {}; - -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => ( - - - - ), -}; - -export default componentMeta; -" -`; diff --git a/addons/docs/src/mdx/mdx-compiler-plugin.js b/addons/docs/src/mdx/mdx-compiler-plugin.js deleted file mode 100644 index be7d6952ba29..000000000000 --- a/addons/docs/src/mdx/mdx-compiler-plugin.js +++ /dev/null @@ -1,365 +0,0 @@ -const mdxToJsx = require('@mdx-js/mdx/mdx-hast-to-jsx'); -const parser = require('@babel/parser'); -const generate = require('@babel/generator').default; -const camelCase = require('lodash/camelCase'); -const jsStringEscape = require('js-string-escape'); - -// Generate the MDX as is, but append named exports for every -// story in the contents - -const STORY_REGEX = /^]/; -const PREVIEW_REGEX = /^]/; -const META_REGEX = /^]/; -const RESERVED = /^(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|await|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$/; - -function getAttr(elt, what) { - const attr = elt.attributes.find((n) => n.name.name === what); - return attr && attr.value; -} - -const isReserved = (name) => RESERVED.exec(name); -const startsWithNumber = (name) => /^\d/.exec(name); - -const sanitizeName = (name) => { - let key = camelCase(name); - if (startsWithNumber(key)) { - key = `_${key}`; - } else if (isReserved(key)) { - key = `${key}Story`; - } - return key; -}; - -const getStoryKey = (name, counter) => (name ? sanitizeName(name) : `story${counter}`); - -function genAttribute(key, element) { - const value = getAttr(element, key); - if (value && value.expression) { - const { code } = generate(value.expression, {}); - return code; - } - return undefined; -} - -function genStoryExport(ast, context) { - let storyName = getAttr(ast.openingElement, 'name'); - let storyId = getAttr(ast.openingElement, 'id'); - storyName = storyName && storyName.value; - storyId = storyId && storyId.value; - - if (!storyId && !storyName) { - throw new Error('Expected a story name or ID attribute'); - } - - // We don't generate exports for story references or the smart "current story" - if (storyId || !storyName) { - return null; - } - - // console.log('genStoryExport', JSON.stringify(ast, null, 2)); - - const statements = []; - const storyKey = getStoryKey(storyName, context.counter); - - const bodyNodes = ast.children.filter((n) => n.type !== 'JSXText'); - let storyCode = null; - let storyVal = null; - if (!bodyNodes.length) { - // plain text node - const { code } = generate(ast.children[0], {}); - storyCode = `'${code}'`; - storyVal = `() => ( - ${storyCode} - )`; - } else { - const bodyParts = bodyNodes.map((bodyNode) => { - const body = bodyNode.type === 'JSXExpressionContainer' ? bodyNode.expression : bodyNode; - const { code } = generate(body, {}); - return { code, body }; - }); - // if we have more than two children - // 1. Add line breaks - // 2. Enclose in <> ... - storyCode = bodyParts.map(({ code }) => code).join('\n'); - const storyReactCode = bodyParts.length > 1 ? `<>\n${storyCode}\n` : storyCode; - // keep track if an indentifier or function call - // avoid breaking change for 5.3 - switch (bodyParts.length === 1 && bodyParts[0].body.type) { - // We don't know what type the identifier is, but this code - // assumes it's a function from CSF. Let's see who complains! - case 'Identifier': - storyVal = `assertIsFn(${storyCode})`; - break; - case 'ArrowFunctionExpression': - storyVal = `(${storyCode})`; - break; - default: - storyVal = `() => ( - ${storyReactCode} - )`; - break; - } - } - - statements.push(`export const ${storyKey} = ${storyVal};`); - - // always preserve the name, since CSF exports can get modified by displayName - statements.push(`${storyKey}.storyName = '${storyName}';`); - - const argTypes = genAttribute('argTypes', ast.openingElement); - if (argTypes) statements.push(`${storyKey}.argTypes = ${argTypes};`); - - const args = genAttribute('args', ast.openingElement); - if (args) statements.push(`${storyKey}.args = ${args};`); - - let parameters = getAttr(ast.openingElement, 'parameters'); - parameters = parameters && parameters.expression; - const source = jsStringEscape(storyCode); - const sourceParam = `storySource: { source: '${source}' }`; - if (parameters) { - const { code: params } = generate(parameters, {}); - statements.push(`${storyKey}.parameters = { ${sourceParam}, ...${params} };`); - } else { - statements.push(`${storyKey}.parameters = { ${sourceParam} };`); - } - - let decorators = getAttr(ast.openingElement, 'decorators'); - decorators = decorators && decorators.expression; - if (decorators) { - const { code: decos } = generate(decorators, {}); - statements.push(`${storyKey}.decorators = ${decos};`); - } - - // eslint-disable-next-line no-param-reassign - context.storyNameToKey[storyName] = storyKey; - - return { - [storyKey]: statements.join('\n'), - }; -} - -function genPreviewExports(ast, context) { - // console.log('genPreviewExports', JSON.stringify(ast, null, 2)); - - const previewExports = {}; - for (let i = 0; i < ast.children.length; i += 1) { - const child = ast.children[i]; - if (child.type === 'JSXElement' && child.openingElement.name.name === 'Story') { - const storyExport = genStoryExport(child, context); - if (storyExport) { - Object.assign(previewExports, storyExport); - // eslint-disable-next-line no-param-reassign - context.counter += 1; - } - } - } - return previewExports; -} - -function genMeta(ast, options) { - let title = getAttr(ast.openingElement, 'title'); - let id = getAttr(ast.openingElement, 'id'); - if (title) { - if (title.type === 'StringLiteral') { - title = "'".concat(jsStringEscape(title.value), "'"); - } else { - try { - // generate code, so the expression is evaluated by the CSF compiler - const { code } = generate(title, {}); - // remove the curly brackets at start and end of code - title = code.replace(/^\{(.+)\}$/, '$1'); - } catch (e) { - // eat exception if title parsing didn't go well - // eslint-disable-next-line no-console - console.warn('Invalid title:', options.filepath); - title = undefined; - } - } - } - id = id && `'${id.value}'`; - const parameters = genAttribute('parameters', ast.openingElement); - const decorators = genAttribute('decorators', ast.openingElement); - const component = genAttribute('component', ast.openingElement); - const subcomponents = genAttribute('subcomponents', ast.openingElement); - const args = genAttribute('args', ast.openingElement); - const argTypes = genAttribute('argTypes', ast.openingElement); - - return { - title, - id, - parameters, - decorators, - component, - subcomponents, - args, - argTypes, - }; -} - -function getExports(node, counter, options) { - const { value, type } = node; - if (type === 'jsx') { - if (STORY_REGEX.exec(value)) { - // Single story - const ast = parser.parseExpression(value, { plugins: ['jsx'] }); - const storyExport = genStoryExport(ast, counter); - return storyExport && { stories: storyExport }; - } - if (PREVIEW_REGEX.exec(value)) { - // Preview, possibly containing multiple stories - const ast = parser.parseExpression(value, { plugins: ['jsx'] }); - return { stories: genPreviewExports(ast, counter) }; - } - if (META_REGEX.exec(value)) { - // Preview, possibly containing multiple stories - const ast = parser.parseExpression(value, { plugins: ['jsx'] }); - return { meta: genMeta(ast, options) }; - } - } - return null; -} - -// insert `mdxStoryNameToKey` and `mdxComponentMeta` into the context so that we -// can reconstruct the Story ID dynamically from the `name` at render time -const wrapperJs = ` -componentMeta.parameters = componentMeta.parameters || {}; -componentMeta.parameters.docs = { - ...(componentMeta.parameters.docs || {}), - page: () => , -}; -`.trim(); - -// Use this rather than JSON.stringify because `Meta`'s attributes -// are already valid code strings, so we want to insert them raw -// rather than add an extra set of quotes -function stringifyMeta(meta) { - let result = '{ '; - Object.entries(meta).forEach(([key, val]) => { - if (val) { - result += `${key}: ${val}, `; - } - }); - result += ' }'; - return result; -} - -const hasStoryChild = (node) => { - if (node.openingElement && node.openingElement.name.name === 'Story') { - return node; - } - if (node.children && node.children.length > 0) { - return node.children.find((child) => hasStoryChild(child)); - } - return null; -}; - -function extractExports(node, options) { - node.children.forEach((child) => { - if (child.type === 'jsx') { - try { - const ast = parser.parseExpression(child.value, { plugins: ['jsx'] }); - if ( - ast.openingElement && - ast.openingElement.type === 'JSXOpeningElement' && - ast.openingElement.name.name === 'Preview' && - !hasStoryChild(ast) - ) { - const previewAst = ast.openingElement; - previewAst.attributes.push({ - type: 'JSXAttribute', - name: { - type: 'JSXIdentifier', - name: 'mdxSource', - }, - value: { - type: 'StringLiteral', - value: encodeURI( - ast.children - .map( - (el) => - generate(el, { - quotes: 'double', - }).code - ) - .join('\n') - ), - }, - }); - } - const { code } = generate(ast, {}); - // eslint-disable-next-line no-param-reassign - child.value = code; - } catch { - /** catch erroneous child.value string where the babel parseExpression makes exception - * https://github.com/mdx-js/mdx/issues/767 - * eg - * generates error - * 1. child.value =`