├── .all-contributorsrc ├── .asyncapi-tool ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── assets │ └── logo.png ├── issue-template.md ├── pull-request-template.md └── workflows │ ├── add-good-first-issue-labels.yml │ ├── automerge-for-humans-add-ready-to-merge-or-do-not-merge-label.yml │ ├── automerge-for-humans-merging.yml │ ├── automerge-for-humans-remove-ready-to-merge-label-on-edit.yml │ ├── automerge-orphans.yml │ ├── automerge.yml │ ├── autoupdate.yml │ ├── bounty-program-commands.yml │ ├── bump.yml │ ├── help-command.yml │ ├── if-nodejs-pr-testing.yml │ ├── if-nodejs-release.yml │ ├── if-nodejs-version-bump.yml │ ├── issues-prs-notifications.yml │ ├── lint-pr-title.yml │ ├── notify-tsc-members-mention.yml │ ├── please-take-a-look-command.yml │ ├── release-announcements.yml │ ├── release-wc-and-playground.yml │ ├── scripts │ ├── README.md │ └── mailchimp │ │ ├── htmlContent.js │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── stale-issues-prs.yml │ ├── transfer-issue.yml │ ├── update-maintainers-trigger.yaml │ ├── update-pr.yml │ └── welcome-first-time-contrib.yml ├── .gitignore ├── .gitpod.yml ├── .markdownlintrc ├── .prettierignore ├── .prettierrc ├── .releaserc ├── .sonarcloud.properties ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── README.md ├── configuration │ └── config-modification.md ├── development │ └── guide.md ├── features │ ├── anchors.md │ └── highlight.md └── usage │ ├── angular.md │ ├── nextjs.md │ ├── standalone-bundle.md │ ├── vue.md │ └── web-component.md ├── lerna.json ├── library ├── cypress.json ├── e2e │ ├── integration │ │ └── standalone.e2e.ts │ ├── plugins │ │ └── index.js │ ├── sites │ │ ├── standalone-v3.html │ │ ├── standalone-without-parser.html │ │ └── standalone.html │ └── tsconfig.json ├── jest.config.js ├── loaders │ └── remove-hashbag-loader.js ├── package.json ├── postcss.config.js ├── src │ ├── __tests__ │ │ ├── docs │ │ │ └── v3 │ │ │ │ ├── adeo-kafka-request-reply.json │ │ │ │ ├── kraken-websocket-request-reply-message-filter-in-reply.json │ │ │ │ ├── kraken-websocket-request-reply-multiple-channels.json │ │ │ │ ├── streetlights-kafka.json │ │ │ │ ├── streetlights-mqtt.json │ │ │ │ └── websocket-gemini.json │ │ └── index.test.tsx │ ├── components │ │ ├── Bindings.tsx │ │ ├── CollapseButton.tsx │ │ ├── Extensions.tsx │ │ ├── Href.tsx │ │ ├── JSONSnippet.tsx │ │ ├── Markdown.tsx │ │ ├── Schema.tsx │ │ ├── Tag.tsx │ │ ├── Tags.tsx │ │ ├── __tests__ │ │ │ ├── Bindings.test.tsx │ │ │ ├── Extensions.test.tsx │ │ │ └── Schema.test.tsx │ │ ├── index.ts │ │ └── supportedExtensions │ │ │ └── XExtension.tsx │ ├── config │ │ ├── config.ts │ │ ├── default.ts │ │ └── index.ts │ ├── constants.ts │ ├── containers │ │ ├── ApplicationErrorHandler │ │ │ └── ErrorBoundary.tsx │ │ ├── AsyncApi │ │ │ ├── AsyncApi.tsx │ │ │ ├── Layout.tsx │ │ │ └── Standalone.tsx │ │ ├── Error │ │ │ └── Error.tsx │ │ ├── Info │ │ │ └── Info.tsx │ │ ├── Messages │ │ │ ├── Message.tsx │ │ │ ├── MessageExample.tsx │ │ │ └── Messages.tsx │ │ ├── Operations │ │ │ ├── Operation.tsx │ │ │ └── Operations.tsx │ │ ├── Schemas │ │ │ ├── Schema.tsx │ │ │ └── Schemas.tsx │ │ ├── Servers │ │ │ ├── Security.tsx │ │ │ ├── Server.tsx │ │ │ └── Servers.tsx │ │ └── Sidebar │ │ │ ├── Sidebar.tsx │ │ │ └── __tests__ │ │ │ └── SideBar.test.tsx │ ├── contexts │ │ ├── index.ts │ │ ├── useConfig.ts │ │ └── useSpec.ts │ ├── helpers │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── schema.test.ts.snap │ │ │ ├── common.test.ts │ │ │ ├── marked.test.ts │ │ │ ├── message.test.ts │ │ │ ├── schema.test.ts │ │ │ ├── sidebar.test.ts │ │ │ └── specification.test.ts │ │ ├── bindingsHelpers.ts │ │ ├── common.ts │ │ ├── index.ts │ │ ├── marked.ts │ │ ├── message.ts │ │ ├── parser.ts │ │ ├── schema.ts │ │ ├── server.ts │ │ ├── sidebar.ts │ │ └── specification.ts │ ├── index.ts │ ├── standalone-codebase.ts │ ├── standalone-without-parser.ts │ ├── standalone.ts │ ├── styles │ │ └── default.css │ ├── types.ts │ └── without-parser.ts ├── tailwind.config.js ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── tsconfig.types.json └── webpack.config.js ├── package-lock.json ├── package.json ├── playground ├── .gitignore ├── README.md ├── app │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── CodeEditorComponent.tsx │ ├── FetchSchema.tsx │ ├── Navigation.tsx │ ├── SplitWrapper.tsx │ ├── Tab.tsx │ ├── Tabs.tsx │ ├── index.ts │ └── styled.ts ├── next.config.mjs ├── package.json ├── postcss.config.js ├── specs │ ├── anyOf.ts │ ├── application-headers.ts │ ├── circular.ts │ ├── correlation-id.ts │ ├── dummy.ts │ ├── gitter-streaming.ts │ ├── index.ts │ ├── invalid.ts │ ├── not.ts │ ├── oneOf.ts │ ├── rpc-client.ts │ ├── rpc-server.ts │ ├── slack-rtm.ts │ └── streetlights.ts ├── tailwind.config.ts ├── tsconfig.json └── utils │ ├── defaultConfig.ts │ ├── helpers.ts │ └── index.ts ├── tsconfig.base.json └── web-component ├── bump-react-comp.sh ├── package.json ├── src ├── AsyncApiWebComponent.tsx └── index.ts ├── tsconfig.json └── webpack.config.js /.asyncapi-tool: -------------------------------------------------------------------------------- 1 | title: AsyncAPI React component 2 | filters: 3 | language: TypeScript 4 | technology: 5 | - React js 6 | - WebComponents 7 | categories: 8 | - ui-component 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest/globals": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended-type-checked", 10 | "plugin:@typescript-eslint/stylistic-type-checked", 11 | "plugin:react/recommended", 12 | "next/core-web-vitals", 13 | "plugin:sonarjs/recommended", 14 | "plugin:jest/recommended", 15 | "prettier" 16 | ], 17 | "plugins": [ 18 | "eslint-plugin-import", 19 | "eslint-plugin-react", 20 | "@typescript-eslint", 21 | "@typescript-eslint/tslint", 22 | "sonarjs", 23 | "jest", 24 | "prettier" 25 | ], 26 | "settings": { 27 | "next": { 28 | "rootDir": "playground/" 29 | } 30 | }, 31 | "parser": "@typescript-eslint/parser", 32 | "parserOptions": { 33 | "project": [ 34 | "./library/tsconfig.json", 35 | "./library/e2e/tsconfig.json", 36 | "./playground/tsconfig.json", 37 | "./web-component/tsconfig.json" 38 | ] 39 | }, 40 | "root": true, 41 | "rules": { 42 | "prettier/prettier": "error", 43 | "jest/expect-expect": [ 44 | "error", 45 | { 46 | "assertFunctionNames": ["expect", "cy"] 47 | } 48 | ] 49 | }, 50 | "ignorePatterns": [ 51 | "library/browser", 52 | "library/lib", 53 | "library/e2e/plugins/index.js", 54 | "library/loaders/remove-hashbag-loader.js", 55 | "library/jest.config.js", 56 | "library/postcss.config.js", 57 | "library/webpack.config.js", 58 | "library/tailwind.config.js", 59 | "playground/out", 60 | "playground/postcss.config.js", 61 | "web-component/lib", 62 | "web-component/webpack.config.js" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | library/package-lock.json -diff 3 | playground/package-lock.json -diff 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in the project 4 | --- 5 | 6 | 10 | 11 | **Description** 12 | 13 | 15 | 16 | 17 | 18 | **Expected result** 19 | 20 | 21 | 22 | **Actual result** 23 | 24 | 25 | 26 | **Steps to reproduce** 27 | 28 | 29 | 30 | **Troubleshooting** 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an improvement to the project 4 | --- 5 | 6 | 10 | 11 | **Description** 12 | 13 | 14 | 15 | **Reasons** 16 | 17 | 18 | 19 | **Attachments** 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asyncapi/asyncapi-react/f0af6c78603c9e35e5d7d54cbccf2a252952b498/.github/assets/logo.png -------------------------------------------------------------------------------- /.github/issue-template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | **Description** 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Description** 8 | 9 | Changes proposed in this pull request: 10 | 11 | - ... 12 | - ... 13 | - ... 14 | 15 | **Related issue(s)** 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/add-good-first-issue-labels.yml: -------------------------------------------------------------------------------- 1 | # This workflow is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # Purpose of this workflow is to enable anyone to label issue with 'Good First Issue' and 'area/*' with a single command. 5 | name: Add 'Good First Issue' and 'area/*' labels # if proper comment added 6 | 7 | on: 8 | issue_comment: 9 | types: 10 | - created 11 | 12 | jobs: 13 | add-labels: 14 | if: ${{(!github.event.issue.pull_request && github.event.issue.state != 'closed' && github.actor != 'asyncapi-bot') && (contains(github.event.comment.body, '/good-first-issue') || contains(github.event.comment.body, '/gfi' ))}} 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Add label 18 | uses: actions/github-script@v7 19 | with: 20 | github-token: ${{ secrets.GH_TOKEN }} 21 | script: | 22 | const areas = ['javascript', 'typescript', 'java' , 'go', 'docs', 'ci-cd', 'design']; 23 | const words = context.payload.comment.body.trim().split(" "); 24 | const areaIndex = words.findIndex((word)=> word === '/gfi' || word === '/good-first-issue') + 1 25 | let area = words[areaIndex]; 26 | switch(area){ 27 | case 'ts': 28 | area = 'typescript'; 29 | break; 30 | case 'js': 31 | area = 'javascript'; 32 | break; 33 | case 'markdown': 34 | area = 'docs'; 35 | break; 36 | } 37 | if(!areas.includes(area)){ 38 | const message = `Hey @${context.payload.sender.login}, your message doesn't follow the requirements, you can try \`/help\`.` 39 | 40 | await github.rest.issues.createComment({ 41 | issue_number: context.issue.number, 42 | owner: context.repo.owner, 43 | repo: context.repo.repo, 44 | body: message 45 | }) 46 | } else { 47 | 48 | // remove area if there is any before adding new labels. 49 | const currentLabels = (await github.rest.issues.listLabelsOnIssue({ 50 | issue_number: context.issue.number, 51 | owner: context.repo.owner, 52 | repo: context.repo.repo, 53 | })).data.map(label => label.name); 54 | 55 | const shouldBeRemoved = currentLabels.filter(label => (label.startsWith('area/') && !label.endsWith(area))); 56 | shouldBeRemoved.forEach(label => { 57 | github.rest.issues.deleteLabel({ 58 | owner: context.repo.owner, 59 | repo: context.repo.repo, 60 | name: label, 61 | }); 62 | }); 63 | 64 | // Add new labels. 65 | github.rest.issues.addLabels({ 66 | issue_number: context.issue.number, 67 | owner: context.repo.owner, 68 | repo: context.repo.repo, 69 | labels: ['good first issue', `area/${area}`] 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/automerge-for-humans-remove-ready-to-merge-label-on-edit.yml: -------------------------------------------------------------------------------- 1 | # This workflow is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # Defence from evil contributor that after adding `ready-to-merge` all suddenly makes evil commit or evil change in PR title 5 | # Label is removed once above action is detected 6 | name: Remove ready-to-merge label 7 | 8 | on: 9 | pull_request_target: 10 | types: 11 | - synchronize 12 | - edited 13 | 14 | jobs: 15 | remove-ready-label: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Remove label 19 | uses: actions/github-script@v7 20 | with: 21 | github-token: ${{ secrets.GH_TOKEN }} 22 | script: | 23 | const labelToRemove = 'ready-to-merge'; 24 | const labels = context.payload.pull_request.labels; 25 | const isLabelPresent = labels.some(label => label.name === labelToRemove) 26 | if(!isLabelPresent) return; 27 | github.rest.issues.removeLabel({ 28 | issue_number: context.issue.number, 29 | owner: context.repo.owner, 30 | repo: context.repo.repo, 31 | name: labelToRemove 32 | }) 33 | -------------------------------------------------------------------------------- /.github/workflows/automerge-orphans.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | name: 'Notify on failing automerge' 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | identify-orphans: 12 | if: startsWith(github.repository, 'asyncapi/') 13 | name: Find orphans and notify 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | - name: Get list of orphans 19 | uses: actions/github-script@v7 20 | id: orphans 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | script: | 24 | const query = `query($owner:String!, $name:String!) { 25 | repository(owner:$owner, name:$name){ 26 | pullRequests(first: 100, states: OPEN){ 27 | nodes{ 28 | title 29 | url 30 | author { 31 | resourcePath 32 | } 33 | } 34 | } 35 | } 36 | }`; 37 | const variables = { 38 | owner: context.repo.owner, 39 | name: context.repo.repo 40 | }; 41 | const { repository: { pullRequests: { nodes } } } = await github.graphql(query, variables); 42 | 43 | let orphans = nodes.filter( (pr) => pr.author.resourcePath === '/asyncapi-bot' || pr.author.resourcePath === '/apps/dependabot') 44 | 45 | if (orphans.length) { 46 | core.setOutput('found', 'true'); 47 | //Yes, this is very naive approach to assume there is just one PR causing issues, there can be a case that more PRs are affected the same day 48 | //The thing is that handling multiple PRs will increase a complexity in this PR that in my opinion we should avoid 49 | //The other PRs will be reported the next day the action runs, or person that checks first url will notice the other ones 50 | core.setOutput('url', orphans[0].url); 51 | core.setOutput('title', orphans[0].title); 52 | } 53 | - if: steps.orphans.outputs.found == 'true' 54 | name: Convert markdown to slack markdown 55 | uses: asyncapi/.github/.github/actions/slackify-markdown@master 56 | id: issuemarkdown 57 | with: 58 | markdown: "-> [${{steps.orphans.outputs.title}}](${{steps.orphans.outputs.url}})" 59 | - if: steps.orphans.outputs.found == 'true' 60 | name: Send info about orphan to slack 61 | uses: rtCamp/action-slack-notify@c33737706dea87cd7784c687dadc9adf1be59990 # Using v2.3.2 62 | env: 63 | SLACK_WEBHOOK: ${{secrets.SLACK_CI_FAIL_NOTIFY}} 64 | SLACK_TITLE: 🚨 Not merged PR that should be automerged 🚨 65 | SLACK_MESSAGE: ${{steps.issuemarkdown.outputs.text}} 66 | MSG_MINIMAL: true -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo. 3 | 4 | name: Automerge PRs from bots 5 | 6 | on: 7 | pull_request_target: 8 | types: 9 | - opened 10 | - synchronize 11 | 12 | jobs: 13 | autoapprove-for-bot: 14 | name: Autoapprove PR comming from a bot 15 | if: > 16 | contains(fromJson('["asyncapi-bot", "dependabot[bot]", "dependabot-preview[bot]"]'), github.event.pull_request.user.login) && 17 | contains(fromJson('["asyncapi-bot", "dependabot[bot]", "dependabot-preview[bot]"]'), github.actor) && 18 | !contains(github.event.pull_request.labels.*.name, 'released') 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Autoapproving 22 | uses: hmarr/auto-approve-action@44888193675f29a83e04faf4002fa8c0b537b1e4 # v3.2.1 is used https://github.com/hmarr/auto-approve-action/releases/tag/v3.2.1 23 | with: 24 | github-token: "${{ secrets.GH_TOKEN_BOT_EVE }}" 25 | 26 | - name: Label autoapproved 27 | uses: actions/github-script@v7 28 | with: 29 | github-token: ${{ secrets.GH_TOKEN }} 30 | script: | 31 | github.rest.issues.addLabels({ 32 | issue_number: context.issue.number, 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | labels: ['autoapproved', 'autoupdate'] 36 | }) 37 | 38 | automerge-for-bot: 39 | name: Automerge PR autoapproved by a bot 40 | needs: [autoapprove-for-bot] 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Automerging 44 | uses: pascalgn/automerge-action@22948e0bc22f0aa673800da838595a3e7347e584 #v0.15.6 https://github.com/pascalgn/automerge-action/releases/tag/v0.15.6 45 | env: 46 | GITHUB_TOKEN: "${{ secrets.GH_TOKEN }}" 47 | GITHUB_LOGIN: asyncapi-bot 48 | MERGE_LABELS: "!do-not-merge" 49 | MERGE_METHOD: "squash" 50 | MERGE_COMMIT_MESSAGE: "{pullRequest.title} (#{pullRequest.number})" 51 | MERGE_RETRIES: "20" 52 | MERGE_RETRY_SLEEP: "30000" 53 | -------------------------------------------------------------------------------- /.github/workflows/autoupdate.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # This workflow is designed to work with: 5 | # - autoapprove and automerge workflows for dependabot and asyncapibot. 6 | # - special release branches that we from time to time create in upstream repos. If we open up PRs for them from the very beginning of the release, the release branch will constantly update with new things from the destination branch they are opened against 7 | 8 | # It uses GitHub Action that auto-updates pull requests branches, whenever changes are pushed to their destination branch. 9 | # Autoupdating to latest destination branch works only in the context of upstream repo and not forks 10 | 11 | name: autoupdate 12 | 13 | on: 14 | push: 15 | branches-ignore: 16 | - 'version-bump/**' 17 | - 'dependabot/**' 18 | - 'bot/**' 19 | - 'all-contributors/**' 20 | 21 | jobs: 22 | autoupdate-for-bot: 23 | if: startsWith(github.repository, 'asyncapi/') 24 | name: Autoupdate autoapproved PR created in the upstream 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Autoupdating 28 | uses: docker://chinthakagodawita/autoupdate-action:v1 29 | env: 30 | GITHUB_TOKEN: '${{ secrets.GH_TOKEN_BOT_EVE }}' 31 | PR_FILTER: "labelled" 32 | PR_LABELS: "autoupdate" 33 | PR_READY_STATE: "ready_for_review" 34 | MERGE_CONFLICT_ACTION: "ignore" 35 | -------------------------------------------------------------------------------- /.github/workflows/bump.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # Purpose of this action is to update npm package in libraries that use it. It is like dependabot for asyncapi npm modules only. 5 | # It runs in a repo after merge of release commit and searches for other packages that use released package. Every found package gets updated with lates version 6 | 7 | name: Bump package version in dependent repos - if Node project 8 | 9 | on: 10 | # It cannot run on release event as when release is created then version is not yet bumped in package.json 11 | # This means we cannot extract easily latest version and have a risk that package is not yet on npm 12 | push: 13 | branches: 14 | - master 15 | 16 | jobs: 17 | bump-in-dependent-projects: 18 | name: Bump this package in repositories that depend on it 19 | if: startsWith(github.event.commits[0].message, 'chore(release):') 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout repo 23 | uses: actions/checkout@v4 24 | - name: Check if Node.js project and has package.json 25 | id: packagejson 26 | run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT 27 | - if: steps.packagejson.outputs.exists == 'true' 28 | name: Bumping latest version of this package in other repositories 29 | uses: derberg/npm-dependency-manager-for-your-github-org@1eafd3bf3974f21d395c1abac855cb04b295d570 # using v6.-.- https://github.com/derberg/npm-dependency-manager-for-your-github-org/releases/tag/v6 30 | with: 31 | github_token: ${{ secrets.GH_TOKEN }} 32 | committer_username: asyncapi-bot 33 | committer_email: info@asyncapi.io 34 | repos_to_ignore: spec,bindings,saunter,server-api 35 | custom_id: "dependency update from asyncapi bot" 36 | -------------------------------------------------------------------------------- /.github/workflows/help-command.yml: -------------------------------------------------------------------------------- 1 | # This workflow is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | name: Create help comment 5 | 6 | on: 7 | issue_comment: 8 | types: 9 | - created 10 | 11 | jobs: 12 | create_help_comment_pr: 13 | if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/help') && github.actor != 'asyncapi-bot' }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Add comment to PR 17 | uses: actions/github-script@v7 18 | with: 19 | github-token: ${{ secrets.GH_TOKEN }} 20 | script: | 21 | //Yes to add comment to PR the same endpoint is use that we use to create a comment in issue 22 | //For more details http://developer.github.com/v3/issues/comments/ 23 | //Also proved by this action https://github.com/actions-ecosystem/action-create-comment/blob/main/src/main.ts 24 | github.rest.issues.createComment({ 25 | issue_number: context.issue.number, 26 | owner: context.repo.owner, 27 | repo: context.repo.repo, 28 | body: `Hello, @${{ github.actor }}! 👋🏼 29 | 30 | I'm 🧞🧞🧞 Genie 🧞🧞🧞 from the magic lamp. Looks like somebody needs a hand! 31 | 32 | At the moment the following comments are supported in pull requests: 33 | 34 | - \`/please-take-a-look\` or \`/ptal\` - This comment will add a comment to the PR asking for attention from the reviewrs who have not reviewed the PR yet. 35 | - \`/ready-to-merge\` or \`/rtm\` - This comment will trigger automerge of PR in case all required checks are green, approvals in place and do-not-merge label is not added 36 | - \`/do-not-merge\` or \`/dnm\` - This comment will block automerging even if all conditions are met and ready-to-merge label is added 37 | - \`/autoupdate\` or \`/au\` - This comment will add \`autoupdate\` label to the PR and keeps your PR up-to-date to the target branch's future changes. Unless there is a merge conflict or it is a draft PR. (Currently only works for upstream branches.) 38 | - \`/update\` or \`/u\` - This comment will update the PR with the latest changes from the target branch. Unless there is a merge conflict or it is a draft PR. NOTE: this only updates the PR once, so if you need to update again, you need to call the command again.` 39 | }) 40 | 41 | create_help_comment_issue: 42 | if: ${{ !github.event.issue.pull_request && startsWith(github.event.comment.body, '/help') && github.actor != 'asyncapi-bot' }} 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Add comment to Issue 46 | uses: actions/github-script@v7 47 | with: 48 | github-token: ${{ secrets.GH_TOKEN }} 49 | script: | 50 | github.rest.issues.createComment({ 51 | issue_number: context.issue.number, 52 | owner: context.repo.owner, 53 | repo: context.repo.repo, 54 | body: `Hello, @${{ github.actor }}! 👋🏼 55 | 56 | I'm 🧞🧞🧞 Genie 🧞🧞🧞 from the magic lamp. Looks like somebody needs a hand! 57 | 58 | At the moment the following comments are supported in issues: 59 | 60 | - \`/good-first-issue {js | ts | java | go | docs | design | ci-cd}\` or \`/gfi {js | ts | java | go | docs | design | ci-cd}\` - label an issue as a \`good first issue\`. 61 | example: \`/gfi js\` or \`/good-first-issue ci-cd\` 62 | - \`/transfer-issue {repo-name}\` or \`/ti {repo-name}\` - transfer issue from the source repository to the other repository passed by the user. example: \`/ti cli\` or \`/transfer-issue cli\`.` 63 | }) -------------------------------------------------------------------------------- /.github/workflows/if-nodejs-pr-testing.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # It does magic only if there is package.json file in the root of the project 5 | name: PR testing - if Node project 6 | 7 | on: 8 | pull_request: 9 | types: [opened, reopened, synchronize, ready_for_review] 10 | 11 | jobs: 12 | test-nodejs-pr: 13 | name: Test NodeJS PR - ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | # Using macos-13 instead of latest (macos-14) due to an issue with Puppeteer and such runner. 18 | # See: https://github.com/puppeteer/puppeteer/issues/12327 and https://github.com/asyncapi/parser-js/issues/1001 19 | os: [ubuntu-latest, macos-13, windows-latest] 20 | steps: 21 | - if: > 22 | !github.event.pull_request.draft && !( 23 | (github.actor == 'asyncapi-bot' && ( 24 | startsWith(github.event.pull_request.title, 'ci: update of files from global .github repo') || 25 | startsWith(github.event.pull_request.title, 'chore(release):') 26 | )) || 27 | (github.actor == 'asyncapi-bot-eve' && ( 28 | startsWith(github.event.pull_request.title, 'ci: update of files from global .github repo') || 29 | startsWith(github.event.pull_request.title, 'chore(release):') 30 | )) || 31 | (github.actor == 'allcontributors[bot]' && 32 | startsWith(github.event.pull_request.title, 'docs: add') 33 | ) 34 | ) 35 | id: should_run 36 | name: Should Run 37 | run: echo "shouldrun=true" >> $GITHUB_OUTPUT 38 | shell: bash 39 | - if: steps.should_run.outputs.shouldrun == 'true' 40 | name: Set git to use LF #to once and for all finish neverending fight between Unix and Windows 41 | run: | 42 | git config --global core.autocrlf false 43 | git config --global core.eol lf 44 | shell: bash 45 | - if: steps.should_run.outputs.shouldrun == 'true' 46 | name: Checkout repository 47 | uses: actions/checkout@v4 48 | - if: steps.should_run.outputs.shouldrun == 'true' 49 | name: Check if Node.js project and has package.json 50 | id: packagejson 51 | run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT 52 | shell: bash 53 | - if: steps.packagejson.outputs.exists == 'true' 54 | name: Check package-lock version 55 | uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master 56 | id: lockversion 57 | - if: steps.packagejson.outputs.exists == 'true' 58 | name: Setup Node.js 59 | uses: actions/setup-node@v4 60 | with: 61 | node-version: "${{ steps.lockversion.outputs.version }}" 62 | - if: steps.lockversion.outputs.version == '18' && matrix.os == 'windows-latest' 63 | #npm cli 10 is buggy because of some cache issue 64 | name: Install npm cli 8 65 | shell: bash 66 | run: npm install -g npm@8.19.4 67 | - if: steps.packagejson.outputs.exists == 'true' 68 | name: Install dependencies 69 | shell: bash 70 | run: npm ci 71 | - if: steps.packagejson.outputs.exists == 'true' 72 | name: Test 73 | run: npm test --if-present 74 | - if: steps.packagejson.outputs.exists == 'true' && matrix.os == 'ubuntu-latest' 75 | #linting should run just one and not on all possible operating systems 76 | name: Run linter 77 | run: npm run lint --if-present 78 | - if: steps.packagejson.outputs.exists == 'true' 79 | name: Run release assets generation to make sure PR does not break it 80 | shell: bash 81 | run: npm run generate:assets --if-present 82 | -------------------------------------------------------------------------------- /.github/workflows/if-nodejs-version-bump.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # It does magic only if there is package.json file in the root of the project 5 | name: Version bump - if Node.js project 6 | 7 | on: 8 | release: 9 | types: 10 | - published 11 | 12 | jobs: 13 | version_bump: 14 | name: Generate assets and bump NodeJS 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | # target branch of release. More info https://docs.github.com/en/rest/reference/repos#releases 21 | # in case release is created from release branch then we need to checkout from given branch 22 | # if @semantic-release/github is used to publish, the minimum version is 7.2.0 for proper working 23 | ref: ${{ github.event.release.target_commitish }} 24 | - name: Check if Node.js project and has package.json 25 | id: packagejson 26 | run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT 27 | - if: steps.packagejson.outputs.exists == 'true' 28 | name: Check package-lock version 29 | uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master 30 | id: lockversion 31 | - if: steps.packagejson.outputs.exists == 'true' 32 | name: Setup Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: "${{ steps.lockversion.outputs.version }}" 36 | cache: 'npm' 37 | cache-dependency-path: '**/package-lock.json' 38 | - if: steps.packagejson.outputs.exists == 'true' 39 | name: Install dependencies 40 | run: npm ci 41 | - if: steps.packagejson.outputs.exists == 'true' 42 | name: Assets generation 43 | run: npm run generate:assets --if-present 44 | - if: steps.packagejson.outputs.exists == 'true' 45 | name: Bump version in package.json 46 | # There is no need to substract "v" from the tag as version script handles it 47 | # When adding "bump:version" script in package.json, make sure no tags are added by default (--no-git-tag-version) as they are already added by release workflow 48 | # When adding "bump:version" script in package.json, make sure --allow-same-version is set in case someone forgot and updated package.json manually and we want to avoide this action to fail and raise confusion 49 | run: VERSION=${{github.event.release.tag_name}} npm run bump:version 50 | - if: steps.packagejson.outputs.exists == 'true' 51 | name: Create Pull Request with updated asset files including package.json 52 | uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # use 4.2.4 https://github.com/peter-evans/create-pull-request/releases/tag/v4.2.4 53 | with: 54 | token: ${{ secrets.GH_TOKEN }} 55 | commit-message: 'chore(release): ${{github.event.release.tag_name}}' 56 | committer: asyncapi-bot 57 | author: asyncapi-bot 58 | title: 'chore(release): ${{github.event.release.tag_name}}' 59 | body: 'Version bump in package.json for release [${{github.event.release.tag_name}}](${{github.event.release.html_url}})' 60 | branch: version-bump/${{github.event.release.tag_name}} 61 | - if: failure() # Only, on failure, send a message on the 94_bot-failing-ci slack channel 62 | name: Report workflow run status to Slack 63 | uses: 8398a7/action-slack@28ba43ae48961b90635b50953d216767a6bea486 #using https://github.com/8398a7/action-slack/releases/tag/v3.16.2 64 | with: 65 | status: ${{ job.status }} 66 | fields: repo,action,workflow 67 | text: 'Unable to bump the version in package.json after the release' 68 | env: 69 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CI_FAIL_NOTIFY }} -------------------------------------------------------------------------------- /.github/workflows/issues-prs-notifications.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # This action notifies community on slack whenever there is a new issue, PR or discussion started in given repository 5 | name: Notify slack 6 | 7 | on: 8 | issues: 9 | types: [opened, reopened] 10 | 11 | pull_request_target: 12 | types: [opened, reopened, ready_for_review] 13 | 14 | discussion: 15 | types: [created] 16 | 17 | jobs: 18 | issue: 19 | if: github.event_name == 'issues' && github.actor != 'asyncapi-bot' && github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' 20 | name: Notify slack on every new issue 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Convert markdown to slack markdown for issue 24 | uses: asyncapi/.github/.github/actions/slackify-markdown@master 25 | id: issuemarkdown 26 | with: 27 | markdown: "[${{github.event.issue.title}}](${{github.event.issue.html_url}}) \n ${{github.event.issue.body}}" 28 | - name: Send info about issue 29 | uses: rtCamp/action-slack-notify@c33737706dea87cd7784c687dadc9adf1be59990 # Using v2.3.2 30 | env: 31 | SLACK_WEBHOOK: ${{secrets.SLACK_GITHUB_NEWISSUEPR}} 32 | SLACK_TITLE: 🐛 New Issue in ${{github.repository}} 🐛 33 | SLACK_MESSAGE: ${{steps.issuemarkdown.outputs.text}} 34 | MSG_MINIMAL: true 35 | 36 | pull_request: 37 | if: github.event_name == 'pull_request_target' && github.actor != 'asyncapi-bot' && github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' 38 | name: Notify slack on every new pull request 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Convert markdown to slack markdown for pull request 42 | uses: asyncapi/.github/.github/actions/slackify-markdown@master 43 | id: prmarkdown 44 | with: 45 | markdown: "[${{github.event.pull_request.title}}](${{github.event.pull_request.html_url}}) \n ${{github.event.pull_request.body}}" 46 | - name: Send info about pull request 47 | uses: rtCamp/action-slack-notify@c33737706dea87cd7784c687dadc9adf1be59990 # Using v2.3.2 48 | env: 49 | SLACK_WEBHOOK: ${{secrets.SLACK_GITHUB_NEWISSUEPR}} 50 | SLACK_TITLE: 💪 New Pull Request in ${{github.repository}} 💪 51 | SLACK_MESSAGE: ${{steps.prmarkdown.outputs.text}} 52 | MSG_MINIMAL: true 53 | 54 | discussion: 55 | if: github.event_name == 'discussion' && github.actor != 'asyncapi-bot' && github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' 56 | name: Notify slack on every new pull request 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Convert markdown to slack markdown for pull request 60 | uses: asyncapi/.github/.github/actions/slackify-markdown@master 61 | id: discussionmarkdown 62 | with: 63 | markdown: "[${{github.event.discussion.title}}](${{github.event.discussion.html_url}}) \n ${{github.event.discussion.body}}" 64 | - name: Send info about pull request 65 | uses: rtCamp/action-slack-notify@c33737706dea87cd7784c687dadc9adf1be59990 # Using v2.3.2 66 | env: 67 | SLACK_WEBHOOK: ${{secrets.SLACK_GITHUB_NEWISSUEPR}} 68 | SLACK_TITLE: 💬 New Discussion in ${{github.repository}} 💬 69 | SLACK_MESSAGE: ${{steps.discussionmarkdown.outputs.text}} 70 | MSG_MINIMAL: true 71 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | name: Lint PR title 5 | 6 | on: 7 | pull_request_target: 8 | types: [opened, reopened, synchronize, edited, ready_for_review] 9 | 10 | jobs: 11 | lint-pr-title: 12 | name: Lint PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Since this workflow is REQUIRED for a PR to be mergable, we have to have this 'if' statement in step level instead of job level. 16 | - if: ${{ !contains(fromJson('["asyncapi-bot", "dependabot[bot]", "dependabot-preview[bot]", "allcontributors[bot]"]'), github.actor) }} 17 | uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 #version 5.2.0 https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.2.0 18 | id: lint_pr_title 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 21 | with: 22 | subjectPattern: ^(?![A-Z]).+$ 23 | subjectPatternError: | 24 | The subject "{subject}" found in the pull request title "{title}" should start with a lowercase character. 25 | 26 | # Comments the error message from the above lint_pr_title action 27 | - if: ${{ always() && steps.lint_pr_title.outputs.error_message != null && !contains(fromJson('["asyncapi-bot", "dependabot[bot]", "dependabot-preview[bot]", "allcontributors[bot]"]'), github.actor)}} 28 | name: Comment on PR 29 | uses: marocchino/sticky-pull-request-comment@3d60a5b2dae89d44e0c6ddc69dd7536aec2071cd #use 2.5.0 https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.5.0 30 | with: 31 | header: pr-title-lint-error 32 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 33 | message: | 34 | 35 | We require all PRs to follow [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). 36 | More details 👇🏼 37 | ``` 38 | ${{ steps.lint_pr_title.outputs.error_message}} 39 | ``` 40 | # deletes the error comment if the title is correct 41 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 42 | name: delete the comment 43 | uses: marocchino/sticky-pull-request-comment@3d60a5b2dae89d44e0c6ddc69dd7536aec2071cd #use 2.5.0 https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.5.0 44 | with: 45 | header: pr-title-lint-error 46 | delete: true 47 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 48 | -------------------------------------------------------------------------------- /.github/workflows/please-take-a-look-command.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | # It uses Github actions to listen for comments on issues and pull requests and 5 | # if the comment contains /please-take-a-look or /ptal it will add a comment pinging 6 | # the code-owners who are reviewers for PR 7 | 8 | name: Please take a Look 9 | 10 | on: 11 | issue_comment: 12 | types: [created] 13 | 14 | jobs: 15 | ping-for-attention: 16 | if: > 17 | github.event.issue.pull_request && 18 | github.event.issue.state != 'closed' && 19 | github.actor != 'asyncapi-bot' && 20 | ( 21 | contains(github.event.comment.body, '/please-take-a-look') || 22 | contains(github.event.comment.body, '/ptal') || 23 | contains(github.event.comment.body, '/PTAL') 24 | ) 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Check for Please Take a Look Command 28 | uses: actions/github-script@v7 29 | with: 30 | github-token: ${{ secrets.GH_TOKEN }} 31 | script: | 32 | const prDetailsUrl = context.payload.issue.pull_request.url; 33 | const { data: pull } = await github.request(prDetailsUrl); 34 | const reviewers = pull.requested_reviewers.map(reviewer => reviewer.login); 35 | 36 | const { data: reviews } = await github.rest.pulls.listReviews({ 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | pull_number: context.issue.number 40 | }); 41 | 42 | const reviewersWhoHaveReviewed = reviews.map(review => review.user.login); 43 | 44 | const reviewersWhoHaveNotReviewed = reviewers.filter(reviewer => !reviewersWhoHaveReviewed.includes(reviewer)); 45 | 46 | if (reviewersWhoHaveNotReviewed.length > 0) { 47 | const comment = reviewersWhoHaveNotReviewed.filter(reviewer => reviewer !== 'asyncapi-bot-eve' ).map(reviewer => `@${reviewer}`).join(' '); 48 | await github.rest.issues.createComment({ 49 | issue_number: context.issue.number, 50 | owner: context.repo.owner, 51 | repo: context.repo.repo, 52 | body: `${comment} Please take a look at this PR. Thanks! :wave:` 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/scripts/README.md: -------------------------------------------------------------------------------- 1 | The entire `scripts` directory is centrally managed in [.github](https://github.com/asyncapi/.github/) repository. Any changes in this folder should be done in central repository. -------------------------------------------------------------------------------- /.github/workflows/scripts/mailchimp/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is centrally managed in https://github.com/asyncapi/.github/ 3 | * Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 4 | */ 5 | const mailchimp = require('@mailchimp/mailchimp_marketing'); 6 | const core = require('@actions/core'); 7 | const htmlContent = require('./htmlContent.js'); 8 | 9 | /** 10 | * Sending API request to mailchimp to schedule email to subscribers 11 | * Input is the URL to issue/discussion or other resource 12 | */ 13 | module.exports = async (link, title) => { 14 | 15 | let newCampaign; 16 | 17 | mailchimp.setConfig({ 18 | apiKey: process.env.MAILCHIMP_API_KEY, 19 | server: 'us12' 20 | }); 21 | 22 | /* 23 | * First we create campaign 24 | */ 25 | try { 26 | newCampaign = await mailchimp.campaigns.create({ 27 | type: 'regular', 28 | recipients: { 29 | list_id: '6e3e437abe', 30 | segment_opts: { 31 | match: 'any', 32 | conditions: [{ 33 | condition_type: 'Interests', 34 | field: 'interests-2801e38b9f', 35 | op: 'interestcontains', 36 | value: ['f7204f9b90'] 37 | }] 38 | } 39 | }, 40 | settings: { 41 | subject_line: `TSC attention required: ${ title }`, 42 | preview_text: 'Check out the latest topic that TSC members have to be aware of', 43 | title: `New topic info - ${ new Date(Date.now()).toUTCString()}`, 44 | from_name: 'AsyncAPI Initiative', 45 | reply_to: 'info@asyncapi.io', 46 | } 47 | }); 48 | } catch (error) { 49 | return core.setFailed(`Failed creating campaign: ${ JSON.stringify(error) }`); 50 | } 51 | 52 | /* 53 | * Content of the email is added separately after campaign creation 54 | */ 55 | try { 56 | await mailchimp.campaigns.setContent(newCampaign.id, { html: htmlContent(link, title) }); 57 | } catch (error) { 58 | return core.setFailed(`Failed adding content to campaign: ${ JSON.stringify(error) }`); 59 | } 60 | 61 | /* 62 | * We schedule an email to send it immediately 63 | */ 64 | try { 65 | //schedule for next hour 66 | //so if this code was created by new issue creation at 9:46, the email is scheduled for 10:00 67 | //is it like this as schedule has limitiations and you cannot schedule email for 9:48 68 | const scheduleDate = new Date(Date.parse(new Date(Date.now()).toISOString()) + 1 * 1 * 60 * 60 * 1000); 69 | scheduleDate.setUTCMinutes(00); 70 | 71 | await mailchimp.campaigns.schedule(newCampaign.id, { 72 | schedule_time: scheduleDate.toISOString(), 73 | }); 74 | } catch (error) { 75 | return core.setFailed(`Failed scheduling email: ${ JSON.stringify(error) }`); 76 | } 77 | 78 | core.info(`New email campaign created`); 79 | } -------------------------------------------------------------------------------- /.github/workflows/scripts/mailchimp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schedule-email", 3 | "description": "This code is responsible for scheduling an email campaign. This file is centrally managed in https://github.com/asyncapi/.github/", 4 | "license": "Apache 2.0", 5 | "dependencies": { 6 | "@actions/core": "1.6.0", 7 | "@mailchimp/mailchimp_marketing": "3.0.74" 8 | } 9 | } -------------------------------------------------------------------------------- /.github/workflows/stale-issues-prs.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | name: Manage stale issues and PRs 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | stale: 12 | if: startsWith(github.repository, 'asyncapi/') 13 | name: Mark issue or PR as stale 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 #v9.1.0 but pointing to commit for security reasons 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: | 20 | This issue has been automatically marked as stale because it has not had recent activity :sleeping: 21 | 22 | It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation. 23 | 24 | There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under [open governance model](https://github.com/asyncapi/community/blob/master/CHARTER.md). 25 | 26 | Let us figure out together how to push this issue forward. Connect with us through [one of many communication channels](https://github.com/asyncapi/community/issues/1) we established here. 27 | 28 | Thank you for your patience :heart: 29 | stale-pr-message: | 30 | This pull request has been automatically marked as stale because it has not had recent activity :sleeping: 31 | 32 | It will be closed in 120 days if no further activity occurs. To unstale this pull request, add a comment with detailed explanation. 33 | 34 | There can be many reasons why some specific pull request has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under [open governance model](https://github.com/asyncapi/community/blob/master/CHARTER.md). 35 | 36 | Let us figure out together how to push this pull request forward. Connect with us through [one of many communication channels](https://github.com/asyncapi/community/issues/1) we established here. 37 | 38 | Thank you for your patience :heart: 39 | days-before-stale: 120 40 | days-before-close: 120 41 | stale-issue-label: stale 42 | stale-pr-label: stale 43 | exempt-issue-labels: keep-open 44 | exempt-pr-labels: keep-open 45 | close-issue-reason: not_planned 46 | -------------------------------------------------------------------------------- /.github/workflows/transfer-issue.yml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | name: Transfer Issues between repositories 5 | 6 | on: 7 | issue_comment: 8 | types: 9 | - created 10 | 11 | permissions: 12 | issues: write 13 | 14 | jobs: 15 | transfer: 16 | if: ${{(!github.event.issue.pull_request && github.event.issue.state != 'closed' && github.actor != 'asyncapi-bot') && (startsWith(github.event.comment.body, '/transfer-issue') || startsWith(github.event.comment.body, '/ti'))}} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v4 21 | - name: Extract Input 22 | id: extract_step 23 | env: 24 | COMMENT: "${{ github.event.comment.body }}" 25 | run: | 26 | REPO=$(echo $COMMENT | awk '{print $2}') 27 | echo repo=$REPO >> $GITHUB_OUTPUT 28 | - name: Check Repo 29 | uses: actions/github-script@v7 30 | with: 31 | github-token: ${{secrets.GH_TOKEN}} 32 | script: | 33 | const r = "${{github.repository}}" 34 | const [owner, repo] = r.split('/') 35 | const repoToMove = process.env.REPO_TO_MOVE 36 | const issue_number = context.issue.number 37 | try { 38 | const {data} = await github.rest.repos.get({ 39 | owner, 40 | repo: repoToMove 41 | }) 42 | }catch (e) { 43 | const body = `${repoToMove} is not a repo under ${owner}. You can only transfer issue to repos that belong to the same organization.` 44 | await github.rest.issues.createComment({ 45 | owner, 46 | repo, 47 | issue_number, 48 | body 49 | }) 50 | process.exit(1) 51 | } 52 | env: 53 | REPO_TO_MOVE: ${{steps.extract_step.outputs.repo}} 54 | - name: Transfer Issue 55 | id: transferIssue 56 | working-directory: ./ 57 | run: | 58 | gh issue transfer ${{github.event.issue.number}} asyncapi/${{steps.extract_step.outputs.repo}} 59 | env: 60 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | -------------------------------------------------------------------------------- /.github/workflows/update-maintainers-trigger.yaml: -------------------------------------------------------------------------------- 1 | # This action is centrally managed in https://github.com/asyncapi/.github/ 2 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo 3 | 4 | name: Trigger MAINTAINERS.yaml file update 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | paths: 10 | # Check all valid CODEOWNERS locations: 11 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location 12 | - 'CODEOWNERS' 13 | - '.github/CODEOWNERS' 14 | - '.docs/CODEOWNERS' 15 | 16 | jobs: 17 | trigger-maintainers-update: 18 | name: Trigger updating MAINTAINERS.yaml because of CODEOWNERS change 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Repository Dispatch 23 | uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # https://github.com/peter-evans/repository-dispatch/releases/tag/v3.0.0 24 | with: 25 | # The PAT with the 'public_repo' scope is required 26 | token: ${{ secrets.GH_TOKEN }} 27 | repository: ${{ github.repository_owner }}/community 28 | event-type: trigger-maintainers-update 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /**/node_modules 3 | 4 | # testing 5 | /**/coverage 6 | /**/cypress 7 | 8 | # production 9 | /**/lib 10 | /**/build 11 | /**/dist 12 | /library/browser 13 | /library/styles 14 | /library/LICENSE 15 | /library/README.md 16 | 17 | # misc 18 | .DS_Store 19 | npm-debug.log* 20 | lerna-debug.log* 21 | /**/*.tgz 22 | /.idea/ 23 | .vscode 24 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # List the start up tasks. Learn more https://www.gitpod.io/docs/config-start-tasks/ 2 | tasks: 3 | - init: npm install # runs during prebuild 4 | command: npm start 5 | 6 | # List the ports to expose. Learn more https://www.gitpod.io/docs/config-ports/ 7 | ports: 8 | - port: 3000 9 | onOpen: open-preview 10 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD007": { 4 | "indent": 4 5 | }, 6 | "line-length": false 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /**/node_modules 3 | 4 | # testing 5 | /**/coverage 6 | 7 | # production 8 | /**/lib 9 | /**/bundles 10 | /**/build 11 | /**/dist 12 | /library/browser 13 | /library/lib 14 | /library/styles 15 | 16 | # misc 17 | .DS_Store 18 | npm-debug.log* 19 | lerna-debug.log* 20 | /**/*.tgz 21 | /.github 22 | 23 | playground/.next/ 24 | playground/out/ 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "bracketSpacing": true, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | --- 2 | branches: 3 | - master 4 | # by default release workflow reacts on push not only to master. 5 | #This is why out of the box sematic release is configured for all these branches 6 | - name: next-spec 7 | prerelease: true 8 | - name: next-major 9 | prerelease: true 10 | - name: next-major-spec 11 | prerelease: true 12 | - name: beta 13 | prerelease: true 14 | - name: alpha 15 | prerelease: true 16 | plugins: 17 | - - "@semantic-release/commit-analyzer" 18 | - preset: conventionalcommits 19 | - - "@semantic-release/release-notes-generator" 20 | - preset: conventionalcommits 21 | - - "@semantic-release/npm" 22 | - pkgRoot: library 23 | - "@semantic-release/github" 24 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.exclusions=library/src/components/Schema.tsx,library/src/helpers/schema.ts,library/src/containers/Error/Error.tsx,library/e2e/**/* 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file provides an overview of code owners in the "asyncapi-react" repository. 2 | 3 | # Each line is a file pattern followed by one or more owners. 4 | # The last matching pattern has the most precedence. 5 | # For more details, read the following article on GitHub: https://help.github.com/articles/about-codeowners/. 6 | 7 | # These are the default owners for the whole content of the "@asyncapi/asyncapi-react" repository. The default owners are automatically added as reviewers when you open a pull request unless different owners are specified in the file. 8 | * @magicmatatjahu @fmvilas @asyncapi-bot-eve @AceTheCreator 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fmvilas@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This directory contains the following documents that relate to the project: 4 | 5 | - configuration: 6 | - [Configuration Modification](./configuration/config-modification.md) describes the available component configuration options. 7 | - development: 8 | - [Development Guide](./development/guide.md) describes how to set up a development environment, write, and run tests. It also contains the naming and architecture convention. 9 | - features: 10 | - [Anchors](./features/anchors.md) describes internal anchors in the AsyncAPI component. 11 | - [Highlight markdown's code blocks](./features/highlight.md) describes how to add custom language configuration for `highlight.js` used in the component. 12 | - usage: 13 | - [Using in Angular](./usage/angular.md) describes how to use the AsyncAPI component in Angular project. 14 | - [Using in Vue](./usage/vue.md) describes how to use the AsyncAPI component in Vue project. 15 | - [Using in NextJS](./usage/nextjs.md) describes how to use the AsyncAPI component in NextJS project. 16 | - [Standalone bundle usage](./usage/standalone-bundle.md) describes how to use the Standalone Bundle of AsyncAPI component with [ReactDOM](https://reactjs.org/docs/react-dom.html) package onboard. 17 | - [Web Component usage](./usage/web-component.md) describes how to use the `web-component`. 18 | -------------------------------------------------------------------------------- /docs/features/anchors.md: -------------------------------------------------------------------------------- 1 | # Anchors 2 | 3 | ## Overview 4 | 5 | The anchors provide the possibility to point to a particular part of a schema. 6 | 7 | ## Details 8 | 9 | Each component has a unique anchor in the `http://{URL}/#{SCHEMA_NAME}-{CONTAINER}-{ITEM_NAME}-{ITEM_PROPERTY}` format, where: 10 | 11 | - `{URL}` is the website URL. 12 | - `{SCHEMA_NAME}` is the name of the schema. It is passed to the component by configuration with the [`schemaID`](../configuration/config-modification.md#definition) field. By default, the value of `schemaID` is set to an empty string. 13 | - `{CONTAINER}` is the name of the container. The possible values are: `info`, `servers`, `operations`, `messages`, `schemas`. If `ITEM_NAME` is also present then container has singular form like `operation`, `message` etc. 14 | - `{ITEM_NAME}` is the name of the item: 15 | - there are no defined items for the `info` container 16 | - for `operations` container, each item has includes type of operation (`publish` or `subscribe`) like `operation-publish-{ITEM_NAME}-...` 17 | - `{ITEM_PROPERTY}` is the name of a particular part of the item. The possible values are: 18 | - for the `info` container: there are no defined properties 19 | - for the `servers` container: `url-variables`, `security` 20 | - for the `operations` container: `parameters`, `message` 21 | - for the `messages` container: `payload`, `headers` 22 | - for the `schemas` container: there are no defined properties 23 | 24 | ## Examples 25 | 26 | - `http://{URL}/#operations` is an anchor that points to the `operations` container. 27 | - `http://{URL}/#custom-spec-operations` is an anchor that points to the `operations` container of the `custom-spec` specification. 28 | - `http://{URL}/#operation-subscribe-testUrl` is an anchor that points to the `testUrl` item of the `operations` container. 29 | - `http://{URL}/#operation-publish-testUrl-parameters` - is an anchor that points to the `parameters` part of the `testUrl` item of the `operations` container. 30 | -------------------------------------------------------------------------------- /docs/features/highlight.md: -------------------------------------------------------------------------------- 1 | # Highlight markdown's code blocks 2 | 3 | ## Overview 4 | 5 | The component internally uses [`highlight.js`](https://highlightjs.org/) to highlight the markdown code blocks. To reduce the size of the component, it only uses the configuration for 3 languages: `json`, `yaml` and `bash`. The rest are rendered as plain text. However, the package provides the instance of the [`highlight.js`](https://highlightjs.org/) outside the package with which you can, for example, add a custom language configuration. 6 | 7 | ## Usage 8 | 9 | To add configuration for an additional language, import configuration from `highlight.js/lib/languages/{LANGUAGE}` where `LANGUAGE` is a language name and load it using the `registerLanguage` function. [There](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md) is a list of supported languages. 10 | 11 | See how to add a configuration in the example below: 12 | 13 | ```js 14 | import AsyncApiComponent, { hljs } from "@asyncapi/react-component"; 15 | 16 | import csharp from 'highlight.js/lib/languages/csharp'; 17 | hljs.registerLanguage('csharp', csharp); 18 | 19 | // And then you can use the comppnent. 20 | render(, document.getElementById("root")); 21 | ``` 22 | 23 | > **NOTE**: You need to load the configuration before rendering the component. 24 | -------------------------------------------------------------------------------- /docs/usage/nextjs.md: -------------------------------------------------------------------------------- 1 | # Using in NextJS 2 | 3 | Read the document to find out how to use React AsyncAPI component in the NextJS project. You can generate static documentation using the component or embed a dynamic component (for dynamic data) in your application. 4 | 5 | ## Prerequisites 6 | 7 | First read the [Readme](../../Readme.md) document and install the React AsyncAPI component by: 8 | 9 | ```sh 10 | npm install --save @asyncapi/react-component@next 11 | ``` 12 | 13 | ## Usage without SSR/SSG (dynamic documentation) 14 | 15 | As mentioned in the introduction, the component can generate static documentation or be embedded in the application as a dynamic component for dynamic data. 16 | 17 | The `@asyncapi/react-component` package has a special `umd` bundle that can be used in the NextJS project. Unfortunately, it only works in the browser itself (due to some dependencies). Due to the universal/isomorphic nature (JS code can run on the server and client side) of NextJS, it is recommended to use the component as below: 18 | 19 | ```jsx 20 | // Import component using `dynamic` helper 21 | import dynamic from 'next/dynamic'; 22 | 23 | // Import styles 24 | import "@asyncapi/react-component/styles/default.min.css"; 25 | 26 | // Import component without SSR/SSG 27 | const AsyncApiComponent = dynamic(() => import('@asyncapi/react-component/browser'), { ssr: false }); 28 | 29 | // `schema` and `config` are these same properties as for normal AsyncAPI React component 30 | export default function AsyncApiDocs({ schema, config }) { 31 | // Render on the browser only 32 | if (typeof navigator === 'undefined') return null; 33 | return schema && ; 34 | } 35 | ``` 36 | 37 | ## Usage with SSR/SSG (static documentation) 38 | 39 | To generate static documentation (and then hydrate it), we need to download an [AsyncAPI Parser](https://github.com/asyncapi/parser-js) that will validate and parse our specs before rendering it - by default, the `@asyncapi/react-component/browser` bundle has a parser built into the component. 40 | 41 | ```sh 42 | npm install --save @asyncapi/parser 43 | ``` 44 | 45 | Then we need to use the parser and component as follows: 46 | 47 | ```js 48 | import { parse } from "@asyncapi/parser"; 49 | // import component without parser onboard 50 | import { AsyncApiComponentWP } from "@asyncapi/react-component"; 51 | 52 | // Import styles 53 | import "@asyncapi/react-component/styles/default.min.css"; 54 | 55 | export default function AsyncApiDocsPage({ asyncapi }) { 56 | const config = {}; // Configuration for component. This same as for normal React component 57 | return ( 58 | 59 | ) 60 | } 61 | 62 | // This function gets called at build time 63 | export async function getStaticProps() { 64 | const schema = `...`; // AsyncAPI specification, fetched or pasted. 65 | 66 | // validate and parse 67 | const parsed = await parse(schema); 68 | // Circular references are not supported. See https://github.com/asyncapi/parser-js/issues/293 69 | const stringified = JSON.stringify(parsed.json()); 70 | 71 | return { 72 | props: { 73 | asyncapi: stringified, 74 | }, 75 | } 76 | } 77 | ``` 78 | 79 | Some benefits using above solution: 80 | 81 | - specification is validated and parsed in build time of page, thus avoiding unnecessary operations on the browser side. Finally rendering is faster. 82 | - final bundle of page is much less. The component with the parser weights around 700kb compared to 150kb without. 83 | -------------------------------------------------------------------------------- /docs/usage/standalone-bundle.md: -------------------------------------------------------------------------------- 1 | # Standalone bundle 2 | 3 | If you are not using React you may want to use the Standalone Bundle of AsyncAPI component with [ReactDOM](https://reactjs.org/docs/react-dom.html) package onboard. It makes it possible to render a component in any other web framework of your choice or in the static HTML webpage. 4 | 5 | ## Installation 6 | 7 | Run this command to install the component in your project: 8 | 9 | ```sh 10 | npm install --save @asyncapi/react-component 11 | ``` 12 | 13 | ## Usage in frameworks 14 | 15 | Check how to use the Standalone bundle in: 16 | 17 | - [Angular](./angular.md#standalone-bundle) 18 | - [Vue](./vue.md) 19 | 20 | ## Usage in HTML webpage 21 | 22 | ```html 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 46 | 47 | 48 | 49 | ``` 50 | 51 | The Standalone Bundle exports two functions: 52 | 53 | - `render`, which works like [ReactDOM.render](https://reactjs.org/docs/react-dom.html#render) function, where first argument is the [props for component](../../README.md#props) and the second is the HTML Node, 54 | - `hydrate`, which works like [ReactDOM.hydrate](https://reactjs.org/docs/react-dom.html#hydrate) function, where first argument is the [props for component](../../README.md#props) and the second is the HTML Node. 55 | 56 | > **NOTE**: The Standalone Bundle takes this [same props](../../README.md#props) as the normal React component. 57 | 58 | > **NOTE**: If there are several components on one page, each one will be rendered using its own properties. 59 | 60 | > **NOTE**: If you don't need [ParserJS](https://github.com/asyncapi/parser-js) inside component, you can use the bundle from `.../browser/standalone/without-parser.js` path of the package. 61 | -------------------------------------------------------------------------------- /docs/usage/vue.md: -------------------------------------------------------------------------------- 1 | # Using in Vue 2 | 3 | If you wanna use the React AsyncAPI component in your Vue project, you may want to use the `AsyncApiStandalone` bundle with `React` and `ReactDom` onboard. 4 | 5 | ## Prerequisites 6 | 7 | First read the [Readme](../../Readme.md) document and install the React AsyncAPI component by: 8 | 9 | ```sh 10 | npm install --save @asyncapi/react-component 11 | ``` 12 | 13 | ## Usage 14 | 15 | To use component in Angular, follow these steps: 16 | 17 | 1. Import `@asyncapi/react-component/browser/standalone` bundle and create similar component as below: 18 | 19 | ```html 20 | 23 | 24 | 41 | ``` 42 | 43 | 2. Copy styles `@asyncapi/react-component/styles/default.min.css` to `assets/asyncapi.min.css` and import in component as: 44 | 45 | ```html 46 | 47 | ``` 48 | 49 | 3. Use the component in your application. 50 | -------------------------------------------------------------------------------- /docs/usage/web-component.md: -------------------------------------------------------------------------------- 1 | # Web Component 2 | 3 | If you are not using React you may want to use the `@asyncapi/web-component` component as a plain web component in any other web framework of your choice or as an element of a static HTML webpage. This is achieved by making use of [web-react-components](https://www.npmjs.com/package/web-react-components). 4 | 5 | ## Installation 6 | 7 | Run this command to install the component in your project: 8 | 9 | ```sh 10 | npm install --save @asyncapi/web-component 11 | ``` 12 | 13 | Check out this simple sandbox application that uses the Web Component in Angular project: 14 | 15 | [![Edit asyncapi-web-component-in-action](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/asyncapi-web-component-in-action-l652x) 16 | 17 | ## Usage in frameworks 18 | 19 | Check how to use the Web Component in: 20 | 21 | - [Angular](./angular.md#web-component) 22 | 23 | ## Usage in HTML webpage 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 34 | cssImportPath="https://unpkg.com/@asyncapi/react-component@0.24.0/styles/default.min.css"> 35 | 36 | ``` 37 | 38 | > **NOTE**: [webcomponentsjs](https://www.npmjs.com/package/@webcomponents/webcomponentsjs) is a series of polyfills to make code runnable in old browsers. It is **optional** if you do not intend to support any. 39 | 40 | > **NOTE**: If a Web Component is called with no properties at all, the error will be shown on the page. 41 | 42 | > **NOTE**: If there are several Web Components on one page, each one will be rendered using its own properties. 43 | 44 | ## Props 45 | 46 | When invoked as an independent entity, Web Component takes the following props (as it is still a React component): 47 | 48 | - `schema` is a `schema` property from the React component, 49 | > **NOTE**: Since version 0.19.0 specifying a `schema` object can be omitted. `schema.url` and `schema.requestOptions` can be replaced with `schemaUrl` and `schemaFetchOptions` properties accordingly. 50 | - `config` is an **optional** `config` property from the React component in the stringified JSON format, 51 | - `schemaUrl` property is a `string`, specific to Web Component, containing a URL to fetch an AsyncAPI Schema from. It is a wrapper for `schema.url` property in `schema` object under the hood, 52 | > **NOTE**: If `schemaUrl` property is specified, the `schema.url` property of the React component will be ignored. 53 | - `schemaFetchOptions` property is an **optional** `object` of [RequestInit](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules_typedoc_node_modules_typescript_lib_lib_dom_d_.requestinit.html) type in JSON format, specific to Web Component, containing additional fetching options. It is a wrapper for `schema.requestOptions` property in `schema` object, which are both needed only in case process of fetching from a URL is any different from a usual browser request, 54 | > **NOTE**: If `schemaFetchOptions` property is specified, `schema.requestOptions` property of the React component will be ignored. If `schemaUrl` property is NOT specified, `schemaFetchOptions` will be ignored itself and `schema.url`/`schema.requestOptions` properties of the React component must be used in this case. 55 | - `cssImportPath` property is the path to styles. Default version from `unpkg.com` contains guaranteed minimum styling for the Web Component, 56 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "npmClient": "npm", 4 | "packages": ["library", "playground", "web-component"], 5 | "command": { 6 | "publish": { 7 | "message": "Bump version to %s" 8 | } 9 | }, 10 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 11 | } 12 | -------------------------------------------------------------------------------- /library/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreTestFiles": "*.js.map", 3 | "integrationFolder": "e2e/integration", 4 | "pluginsFile": "e2e/plugins/index.js", 5 | "fixturesFolder": false, 6 | "supportFile": false, 7 | "fileServerFolder": ".", 8 | "video": false, 9 | "viewportWidth": 1440, 10 | "viewportHeight": 720 11 | } 12 | -------------------------------------------------------------------------------- /library/e2e/integration/standalone.e2e.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/valid-title */ 2 | describe('Standalone bundle', () => { 3 | testSuite('With parser', 'e2e/sites/standalone.html'); 4 | testSuite('With parser for v3', 'e2e/sites/standalone-v3.html'); 5 | testSuite('Without parser', 'e2e/sites/standalone-without-parser.html'); 6 | 7 | function testSuite(testName: string, site: string) { 8 | describe(testName, () => { 9 | before(() => { 10 | cy.visit(site); 11 | }); 12 | 13 | it('Container should exist', () => { 14 | cy.get('#spec').should('exist'); 15 | }); 16 | 17 | it('Title of spec should be rendered', () => { 18 | cy.contains('Example AsyncAPI').should('exist'); 19 | }); 20 | 21 | it('Servers of spec should be rendered', () => { 22 | cy.contains('example-server').should('exist'); 23 | }); 24 | 25 | it('Channels of spec should be rendered', () => { 26 | cy.contains('example-channel').should('exist'); 27 | }); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /library/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | const wp = require('@cypress/webpack-preprocessor'); 2 | 3 | const webpackOptions = { 4 | resolve: { 5 | extensions: ['.ts', '.js'], 6 | }, 7 | performance: false, 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.ts$/, 12 | exclude: [/node_modules/], 13 | use: [ 14 | { 15 | loader: 'ts-loader', 16 | options: { 17 | configFile: 'e2e/tsconfig.json', 18 | }, 19 | }, 20 | ], 21 | }, 22 | ], 23 | }, 24 | }; 25 | 26 | module.exports = (on) => { 27 | on('file:preprocessor', wp({ webpackOptions })); 28 | }; 29 | -------------------------------------------------------------------------------- /library/e2e/sites/standalone-v3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /library/e2e/sites/standalone-without-parser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /library/e2e/sites/standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /library/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2015", 7 | "noImplicitAny": false, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "strictNullChecks": true, 11 | "sourceMap": true, 12 | "pretty": true, 13 | "lib": ["es2015", "es2016", "es2017", "dom"], 14 | "jsx": "react", 15 | "types": ["cypress"] 16 | }, 17 | "compileOnSave": false, 18 | "include": ["integration/*.ts", "../node_modules/cypress"] 19 | } 20 | -------------------------------------------------------------------------------- /library/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest', 6 | }, 7 | 8 | rootDir: 'src', 9 | testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], 10 | 11 | globals: { 12 | 'ts-jest': { 13 | tsconfig: 'tsconfig.json', 14 | }, 15 | }, 16 | moduleNameMapper: { 17 | '^nimma/legacy$': 18 | '/../../node_modules/nimma/dist/legacy/cjs/index.js', 19 | '^nimma/(.*)': '/../../node_modules/nimma/dist/cjs/$1', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /library/loaders/remove-hashbag-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Make sure code does not contain properties such as `#property` 3 | */ 4 | module.exports = function (source) { 5 | return source.replace(/^#! .*\n/, ''); 6 | }; 7 | -------------------------------------------------------------------------------- /library/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | require('postcss-import'), 6 | require('postcss-scopify')('.aui-root'), 7 | process.env.NODE_ENV === 'production' && 8 | process.env.MINIFY_STYLES === 'true' && 9 | require('cssnano')({ 10 | preset: 'default', 11 | }), 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /library/src/components/Bindings.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import React from 'react'; 3 | import { Schema } from './Schema'; 4 | import { SchemaHelpers } from '../helpers'; 5 | import { BindingsInterface } from '@asyncapi/parser'; 6 | 7 | interface Props { 8 | name?: string; 9 | bindings: BindingsInterface; 10 | } 11 | 12 | export const Bindings: React.FunctionComponent = ({ 13 | name = 'Binding specific information', 14 | bindings, 15 | }) => { 16 | if (!bindings || bindings.isEmpty()) { 17 | return null; 18 | } 19 | 20 | const renderedBindings = bindings.all().map((binding) => { 21 | const bindingValue = binding.value(); 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 23 | const schema = SchemaHelpers.jsonToSchema(bindingValue); 24 | const protocol = binding.protocol(); 25 | const schemaName = ( 26 |
27 | {name} 28 | 29 | {protocol} 30 | 31 |
32 | ); 33 | return ( 34 | schema !== undefined && ( 35 | 41 | ) 42 | ); 43 | }); 44 | 45 | return <>{renderedBindings}; 46 | }; 47 | -------------------------------------------------------------------------------- /library/src/components/CollapseButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, SVGAttributes } from 'react'; 2 | 3 | interface Props extends ButtonHTMLAttributes { 4 | chevronProps?: SVGAttributes; 5 | expanded?: boolean; 6 | } 7 | 8 | const HiChevronRight = (props: SVGAttributes = {}) => ( 9 | // Copied from https://icon-sets.iconify.design/heroicons-solid/chevron-right/ 10 | 20 | 25 | 26 | ); 27 | 28 | export const CollapseButton: React.FunctionComponent = ({ 29 | chevronProps, 30 | expanded = false, 31 | children, 32 | ...rest 33 | }) => ( 34 | 47 | ); 48 | -------------------------------------------------------------------------------- /library/src/components/Extensions.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | 4 | import React, { useState } from 'react'; 5 | 6 | import { Schema } from './Schema'; 7 | 8 | import { SchemaHelpers } from '../helpers'; 9 | import { useConfig, useSpec } from '../contexts'; 10 | import { CollapseButton } from './CollapseButton'; 11 | 12 | interface Props { 13 | name?: string; 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | item: any; 16 | } 17 | 18 | export const Extensions: React.FunctionComponent = ({ 19 | name = 'Extensions', 20 | item, 21 | }) => { 22 | const [expanded, setExpanded] = useState(false); 23 | 24 | const config = useConfig(); 25 | const document = useSpec(); 26 | 27 | const extensions = SchemaHelpers.getCustomExtensions(item); 28 | if (!extensions || !Object.keys(extensions).length) { 29 | return null; 30 | } 31 | 32 | if (!config.extensions || !Object.keys(config.extensions).length) { 33 | const schema = SchemaHelpers.jsonToSchema(extensions); 34 | return ( 35 | schema && ( 36 |
37 | 38 |
39 | ) 40 | ); 41 | } 42 | 43 | return ( 44 |
45 |
46 |
47 | <> 48 | setExpanded((prev) => !prev)} 50 | expanded={expanded} 51 | > 52 | {name} 53 | 54 | 55 |
56 |
57 |
60 | {Object.keys(extensions) 61 | .sort((extension1, extension2) => 62 | extension1.localeCompare(extension2), 63 | ) 64 | .map((extensionKey) => { 65 | if (config.extensions?.[extensionKey]) { 66 | const CustomExtensionComponent = config.extensions[extensionKey]; 67 | return ( 68 | 75 | ); 76 | } else { 77 | const extensionSchema = SchemaHelpers.jsonToSchema( 78 | extensions[extensionKey], 79 | ); 80 | return ( 81 |
82 | 83 |
84 | ); 85 | } 86 | })} 87 |
88 |
89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /library/src/components/Href.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | href: string; 5 | title?: string; 6 | className?: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | export const Href: React.FunctionComponent = ({ 11 | href, 12 | title, 13 | className, 14 | children, 15 | }) => ( 16 | 23 | {children} 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /library/src/components/JSONSnippet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Markdown } from './index'; 4 | 5 | interface Props { 6 | snippet: string | object; 7 | } 8 | 9 | export const JSONSnippet: React.FunctionComponent = ({ snippet }) => { 10 | if (typeof snippet === 'object') { 11 | // change code to markdown's json code block 12 | snippet = '```json\n' + JSON.stringify(snippet, undefined, 2) + '\n```'; 13 | } 14 | 15 | return {snippet}; 16 | }; 17 | -------------------------------------------------------------------------------- /library/src/components/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { sanitize } from 'isomorphic-dompurify'; 3 | 4 | import { renderMarkdown } from '../helpers'; 5 | 6 | export const Markdown: React.FunctionComponent<{ 7 | children: React.ReactNode; 8 | }> = ({ children }) => { 9 | if (!children) { 10 | return null; 11 | } 12 | if (typeof children !== 'string') { 13 | return <>{children}; 14 | } 15 | 16 | return ( 17 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /library/src/components/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TagInterface } from '@asyncapi/parser'; 3 | 4 | import { Href } from './Href'; 5 | 6 | interface Props { 7 | tag: TagInterface; 8 | } 9 | 10 | export const Tag: React.FunctionComponent = ({ tag }) => { 11 | const name = `#${tag.name()}`; 12 | const description = tag.description() ?? ''; 13 | const externalDocs = tag.externalDocs(); 14 | 15 | const element = ( 16 |
20 | {name} 21 |
22 | ); 23 | 24 | if (externalDocs) { 25 | return ( 26 | 27 | {element} 28 | 29 | ); 30 | } 31 | return element; 32 | }; 33 | -------------------------------------------------------------------------------- /library/src/components/Tags.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TagsInterface } from '@asyncapi/parser'; 3 | 4 | import { Tag } from './Tag'; 5 | 6 | interface Props { 7 | tags?: TagsInterface; 8 | } 9 | 10 | export const Tags: React.FunctionComponent = ({ tags }) => { 11 | if (!tags?.length) { 12 | return null; 13 | } 14 | 15 | return ( 16 |
    17 | {tags.all().map((tag) => ( 18 |
  • 19 | 20 |
  • 21 | ))} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /library/src/components/__tests__/Bindings.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | /* eslint-disable sonarjs/no-duplicate-string */ 6 | 7 | import React from 'react'; 8 | import { render, screen } from '@testing-library/react'; 9 | import { 10 | BindingV2 as BindingSchema, 11 | BindingsV2 as BindingsSchema, 12 | } from '@asyncapi/parser'; 13 | 14 | import { Bindings } from '../Bindings'; 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | function createBinding(bindingObj: Record) { 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | const bindings: BindingSchema[] = []; 19 | for (const [protocol, binding] of Object.entries(bindingObj)) { 20 | const obj: Record = {}; 21 | obj[protocol] = binding; 22 | bindings.push( 23 | new BindingSchema(binding, { 24 | asyncapi: {} as never, 25 | pointer: '', 26 | protocol, 27 | }), 28 | ); 29 | } 30 | return new BindingsSchema(bindings); 31 | } 32 | describe('Bindings component', () => { 33 | test('should work with simple data', () => { 34 | const bindings = { 35 | mqtt: { 36 | fooMqtt: 'barMqtt', 37 | }, 38 | kafka: { 39 | fooKafka: 'barKafka', 40 | }, 41 | }; 42 | render(); 43 | 44 | expect(screen.getAllByText('Binding specific information')).toBeDefined(); 45 | expect(screen.getAllByText('Binding specific information')).toHaveLength(2); 46 | expect(screen.getByText('mqtt')).toBeDefined(); 47 | expect(screen.getByText('fooMqtt')).toBeDefined(); 48 | expect(screen.getByText('barMqtt')).toBeDefined(); 49 | expect(screen.getByText('kafka')).toBeDefined(); 50 | expect(screen.getByText('fooKafka')).toBeDefined(); 51 | expect(screen.getByText('barKafka')).toBeDefined(); 52 | }); 53 | 54 | test('should render empty binding as string', () => { 55 | const bindings = { 56 | mqtt: { 57 | foo: 'bar', 58 | }, 59 | kafka: undefined, 60 | http: null, 61 | }; 62 | render(); 63 | 64 | expect(screen.getAllByText('Binding specific information')).toBeDefined(); 65 | expect(screen.getAllByText('Binding specific information')).toHaveLength(3); 66 | expect(screen.getByText('mqtt')).toBeDefined(); 67 | expect(screen.getByText('foo')).toBeDefined(); 68 | expect(screen.getByText('bar')).toBeDefined(); 69 | expect(screen.getByText('kafka')).toBeDefined(); 70 | expect(screen.getByText('http')).toBeDefined(); 71 | expect(screen.queryByText('undefined')).toEqual(null); 72 | expect(screen.queryByText('null')).toEqual(null); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /library/src/components/__tests__/Extensions.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import React from 'react'; 6 | import { render, screen } from '@testing-library/react'; 7 | 8 | import { SchemaV2 as SchemaModel } from '@asyncapi/parser'; 9 | 10 | import { Extensions } from '../Extensions'; 11 | 12 | describe('Extensions component', () => { 13 | test('should work with simple data', () => { 14 | const schema = { 15 | 'x-foo': 'xBar', 16 | 'x-bar': 'xFoo', 17 | }; 18 | const schemaModel = new SchemaModel(schema); 19 | render(); 20 | 21 | expect(screen.getByText('Extensions')).toBeDefined(); 22 | expect(screen.getByText('x-foo')).toBeDefined(); 23 | expect(screen.getByText('xBar')).toBeDefined(); 24 | expect(screen.getByText('x-bar')).toBeDefined(); 25 | expect(screen.getByText('xFoo')).toBeDefined(); 26 | }); 27 | 28 | test('should filter non extensions', () => { 29 | const schema = { 30 | foo: { 31 | foo: 'bar', 32 | }, 33 | bar: { 34 | foo: 'bar', 35 | }, 36 | 'x-foo': 'xBar', 37 | 'x-bar': 'xFoo', 38 | }; 39 | const schemaModel = new SchemaModel(schema); 40 | render(); 41 | 42 | expect(screen.getByText('Extensions')).toBeDefined(); 43 | expect(screen.getByText('x-foo')).toBeDefined(); 44 | expect(screen.getByText('xBar')).toBeDefined(); 45 | expect(screen.getByText('x-bar')).toBeDefined(); 46 | expect(screen.getByText('xFoo')).toBeDefined(); 47 | expect(screen.queryByText('foo')).toEqual(null); 48 | expect(screen.queryByText('bar')).toEqual(null); 49 | }); 50 | 51 | test('should render empty extension as string', () => { 52 | const schema = { 53 | 'x-foo': 'xBar', 54 | 'x-bar': undefined, 55 | 'x-foobar': null, 56 | }; 57 | const schemaModel = new SchemaModel(schema); 58 | render(); 59 | 60 | expect(screen.getByText('Extensions')).toBeDefined(); 61 | expect(screen.getByText('x-foo')).toBeDefined(); 62 | expect(screen.getByText('xBar')).toBeDefined(); 63 | expect(screen.getByText('x-bar')).toBeDefined(); 64 | expect(screen.getByText('x-foobar')).toBeDefined(); 65 | expect(screen.queryByText('undefined')).toEqual(null); 66 | expect(screen.queryByText('null')).toEqual(null); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /library/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Bindings'; 2 | export * from './CollapseButton'; 3 | export * from './JSONSnippet'; 4 | export * from './Extensions'; 5 | export * from './Href'; 6 | export * from './Markdown'; 7 | export * from './Schema'; 8 | export * from './Tag'; 9 | export * from './Tags'; 10 | -------------------------------------------------------------------------------- /library/src/components/supportedExtensions/XExtension.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ExtensionComponentProps } from '../../types'; 4 | 5 | /** 6 | * See . 7 | */ 8 | export default function XExtension({ 9 | propertyValue, 10 | }: ExtensionComponentProps) { 11 | return ( 12 | 19 | 28 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /library/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionComponentProps } from '../types'; 2 | 3 | export interface ConfigInterface { 4 | schemaID?: string; 5 | show?: ShowConfig; 6 | expand?: ExpandConfig; 7 | sidebar?: SideBarConfig; 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | parserOptions?: any; 10 | publishLabel?: string; 11 | subscribeLabel?: string; 12 | sendLabel?: string; 13 | receiveLabel?: string; 14 | requestLabel?: string; 15 | replyLabel?: string; 16 | extensions?: Record>; 17 | } 18 | 19 | export interface ShowConfig { 20 | sidebar?: boolean; 21 | info?: boolean; 22 | servers?: boolean; 23 | operations?: boolean; 24 | messages?: boolean; 25 | messageExamples?: boolean; 26 | schemas?: boolean; 27 | errors?: boolean; 28 | } 29 | 30 | export interface ExpandConfig { 31 | messageExamples?: boolean; 32 | } 33 | 34 | export interface SideBarConfig { 35 | showServers?: 'byDefault' | 'bySpecTags' | 'byServersTags'; 36 | showOperations?: 'byDefault' | 'bySpecTags' | 'byOperationsTags'; 37 | useChannelAddressAsIdentifier?: boolean; 38 | } 39 | -------------------------------------------------------------------------------- /library/src/config/default.ts: -------------------------------------------------------------------------------- 1 | import { ConfigInterface } from './config'; 2 | import { 3 | PUBLISH_LABEL_DEFAULT_TEXT, 4 | RECEIVE_TEXT_LABEL_DEFAULT_TEXT, 5 | REPLIER_LABEL_DEFAULT_TEXT, 6 | REQUEST_LABEL_DEFAULT_TEXT, 7 | SEND_LABEL_DEFAULT_TEXT, 8 | SUBSCRIBE_LABEL_DEFAULT_TEXT, 9 | } from '../constants'; 10 | import XExtension from '../components/supportedExtensions/XExtension'; 11 | 12 | export const defaultConfig: ConfigInterface = { 13 | schemaID: '', 14 | show: { 15 | sidebar: false, 16 | info: true, 17 | servers: true, 18 | operations: true, 19 | messages: true, 20 | messageExamples: false, 21 | schemas: true, 22 | errors: true, 23 | }, 24 | expand: { 25 | messageExamples: false, 26 | }, 27 | sidebar: { 28 | showServers: 'byDefault', 29 | showOperations: 'byDefault', 30 | }, 31 | publishLabel: PUBLISH_LABEL_DEFAULT_TEXT, 32 | subscribeLabel: SUBSCRIBE_LABEL_DEFAULT_TEXT, 33 | sendLabel: SEND_LABEL_DEFAULT_TEXT, 34 | receiveLabel: RECEIVE_TEXT_LABEL_DEFAULT_TEXT, 35 | requestLabel: REQUEST_LABEL_DEFAULT_TEXT, 36 | replyLabel: REPLIER_LABEL_DEFAULT_TEXT, 37 | extensions: { 38 | 'x-x': XExtension, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /library/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './default'; 3 | -------------------------------------------------------------------------------- /library/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CSS_PREFIX = 'asyncapi'; 2 | export const CONTENT_TYPES_SITE = 3 | 'https://www.iana.org/assignments/media-types'; 4 | export const COLLAPSE_ALL_TEXT = 'Collapse All'; 5 | export const EXPAND_ALL_TEXT = 'Expand All'; 6 | 7 | export const VALIDATION_ERRORS_TYPE = 8 | 'https://github.com/asyncapi/parser-js/validation-errors'; 9 | export const SERVERS = 'Servers'; 10 | 11 | export const ONE_OF_PAYLOADS_TEXT = 'One of those payloads:'; 12 | export const ANY_OF_PAYLOADS_TEXT = 'Any of those payloads:'; 13 | export const GENERATED_BADGE_TEXT = 'generated'; 14 | export const ONE_OF_FOLLOWING_MESSAGES_PUBLISH_TEXT = 15 | 'You can send one of the following messages:'; 16 | export const ONE_OF_FOLLOWING_MESSAGES_PUBLISH_SINGLE_TEXT = 17 | 'You can send the following message:'; 18 | export const ONE_OF_FOLLOWING_MESSAGES_SUBSCRIBE_TEXT = 19 | 'You can subscribe to one of the following messages:'; 20 | export const ONE_OF_FOLLOWING_MESSAGES_SUBSCRIBE_SINGLE_TEXT = 21 | 'You can subscribe to the following message:'; 22 | export const RAW_MESSAGE_SUBSCRIBE_TEXT = 23 | 'You can subscribe to the following message:'; 24 | export const RAW_MESSAGE_PUBLISH_TEXT = 'You can send the following message:'; 25 | 26 | export const CONTACT_TEXT = 'Contact'; 27 | export const NAM_TEXTE = 'Name'; 28 | export const URL_TEXT = 'Url'; 29 | export const EMAIL_TEXT = 'Email'; 30 | export const LICENSE_TEXT = 'License'; 31 | export const TERMS_OF_SERVICE_TEXT = 'Terms of service'; 32 | export const URL_SUPPORT_TEXT = 'Support'; 33 | export const EMAIL_SUPPORT_TEXT = 'Email support'; 34 | export const EXTERAL_DOCUMENTATION_TEXT = 'External Docs'; 35 | export const LOCATION_TEXT = 'Location'; 36 | export const TYPE_TEXT = 'Type'; 37 | export const SPECIFICATION_TEXT = 'Specification'; 38 | 39 | export const DEPRECATED_TEXT = 'Deprecated'; 40 | export const PUBLISH_TEXT = 'Publish'; 41 | export const PUBLISH_LABEL_DEFAULT_TEXT = 'PUB'; 42 | export const SEND_TEXT = 'Send'; 43 | export const SEND_LABEL_DEFAULT_TEXT = 'SEND'; 44 | export const SUBSCRIBE_TEXT = 'Subscribe'; 45 | export const SUBSCRIBE_LABEL_DEFAULT_TEXT = 'SUB'; 46 | export const RECEIVE_TEXT = 'Receive'; 47 | export const RECEIVE_TEXT_LABEL_DEFAULT_TEXT = 'RECEIVE'; 48 | export const REQUEST_TEXT = 'Request'; 49 | export const REQUEST_LABEL_DEFAULT_TEXT = 'REQUEST'; 50 | export const REPLIER_TEXT = 'Reply'; 51 | export const REPLIER_LABEL_DEFAULT_TEXT = 'REPLY'; 52 | export const REQUIRED_TEXT = 'Required'; 53 | export const GENERATED_TEXT = 'Generated'; 54 | 55 | export const SERVERS_TEXT = 'Servers'; 56 | export const OPERATIONS_TEXT = 'Operations'; 57 | export const MESSAGES_TEXT = 'Messages'; 58 | export const SCHEMAS_TEXT = 'Schemas'; 59 | 60 | export const CHANNELS_TEXT = 'Channels'; 61 | export const PARAMETERS_TEXT = 'Parameters'; 62 | export const HEADERS_TEXT = 'Headers'; 63 | export const MESSAGE_HEADERS_TEXT = 'Message Headers'; 64 | export const HEADERS_EXAMPLE_TEXT = 'Example of headers'; 65 | export const TAGS_TEXT = 'Tags'; 66 | export const PAYLOAD_TEXT = 'Payload'; 67 | export const MESSAGE_PAYLOAD_TEXT = 'Message Payload'; 68 | export const PAYLOAD_EXAMPLE_TEXT = 'Example of payload'; 69 | export const SCHEMA_EXAMPLE_TEXT = 'Example'; 70 | 71 | export const SERVER_BINDINGS_TEXT = 'Server Bindings'; 72 | export const CHANNEL_BINDINGS_TEXT = 'Channel Bindings'; 73 | export const OPERATION_BINDINGS_TEXT = 'Operation Bindings'; 74 | export const MESSAGE_BINDINGS_TEXT = 'Message Bindings'; 75 | 76 | export const BINDINGS_SCHEMA_OBJECT_TEXT = 'Schema Object'; 77 | 78 | export const NONE_TEXT = 'None'; 79 | export const ANY_TEXT = 'Any'; 80 | export const ERROR_TEXT = 'Error'; 81 | export const EXPAND_ERROR_BUTTON_TEXT = 'Expand'; 82 | export const COLLAPSE_ERROR_BUTTON_TEXT = 'Collapse'; 83 | 84 | export const SECURITY_TEXT = 'Security'; 85 | 86 | export const URL_VARIABLES_TEXT = 'URL Variables'; 87 | -------------------------------------------------------------------------------- /library/src/containers/ApplicationErrorHandler/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { ReactNode } from 'react'; 3 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; 4 | import { ErrorObject } from '../../types'; 5 | import { Error } from '../Error/Error'; 6 | 7 | interface Props { 8 | children: ReactNode; 9 | } 10 | 11 | function fallbackRender({ error }: FallbackProps) { 12 | const ErrorObject: ErrorObject = { 13 | title: 'Something went wrong', 14 | type: 'application-error', 15 | validationErrors: [ 16 | { 17 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 18 | title: error?.message, 19 | }, 20 | ], 21 | }; 22 | return ; 23 | } 24 | 25 | const AsyncApiErrorBoundary = ({ children }: Props) => { 26 | const [key, setKey] = useState(0); 27 | 28 | useEffect(() => { 29 | setKey((prevKey) => prevKey + 1); 30 | }, [children]); 31 | 32 | return ( 33 | 34 | {children} 35 | 36 | ); 37 | }; 38 | 39 | export default AsyncApiErrorBoundary; 40 | -------------------------------------------------------------------------------- /library/src/containers/AsyncApi/AsyncApi.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AsyncAPIDocumentInterface } from '@asyncapi/parser'; 3 | 4 | import AsyncApiStandalone from './Standalone'; 5 | 6 | import { 7 | isFetchingSchemaInterface, 8 | ErrorObject, 9 | PropsSchema, 10 | } from '../../types'; 11 | import { ConfigInterface } from '../../config'; 12 | import { SpecificationHelpers, Parser } from '../../helpers'; 13 | 14 | export interface AsyncApiProps { 15 | schema: PropsSchema; 16 | config?: Partial; 17 | } 18 | 19 | interface AsyncAPIState { 20 | asyncapi?: AsyncAPIDocumentInterface; 21 | error?: ErrorObject; 22 | } 23 | 24 | class AsyncApiComponent extends Component { 25 | state: AsyncAPIState = { 26 | asyncapi: undefined, 27 | error: undefined, 28 | }; 29 | 30 | async componentDidMount() { 31 | if (this.props.schema) { 32 | const { schema, config } = this.props; 33 | await this.parseSchema(schema, config?.parserOptions); 34 | } 35 | } 36 | 37 | async componentDidUpdate(prevProps: AsyncApiProps) { 38 | const oldSchema = prevProps.schema; 39 | const newSchema = this.props.schema; 40 | 41 | if (oldSchema !== newSchema) { 42 | const { config } = this.props; 43 | await this.parseSchema(newSchema, config?.parserOptions); 44 | } 45 | } 46 | 47 | render() { 48 | const { schema, config } = this.props; 49 | const { asyncapi, error } = this.state; 50 | 51 | return ( 52 | 57 | ); 58 | } 59 | 60 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 61 | private async parseSchema(schema: PropsSchema, parserOptions?: any) { 62 | const parsedSpec = SpecificationHelpers.retrieveParsedSpec(schema); 63 | if (parsedSpec) { 64 | this.setState({ 65 | asyncapi: parsedSpec, 66 | }); 67 | return; 68 | } 69 | 70 | if (isFetchingSchemaInterface(schema)) { 71 | const parsedFromUrl = await Parser.parseFromUrl(schema, parserOptions); 72 | this.setState({ 73 | asyncapi: parsedFromUrl.asyncapi, 74 | error: parsedFromUrl.error, 75 | }); 76 | return; 77 | } 78 | 79 | const parsed = await Parser.parse(schema, parserOptions); 80 | this.setState({ 81 | asyncapi: parsed.asyncapi, 82 | error: parsed.error, 83 | }); 84 | } 85 | } 86 | 87 | export default AsyncApiComponent; 88 | -------------------------------------------------------------------------------- /library/src/containers/AsyncApi/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AsyncAPIDocumentInterface } from '@asyncapi/parser'; 3 | import useResizeObserver from 'use-resize-observer'; 4 | 5 | import { Sidebar } from '../Sidebar/Sidebar'; 6 | import { Info } from '../Info/Info'; 7 | import { Servers } from '../Servers/Servers'; 8 | import { Operations } from '../Operations/Operations'; 9 | import { Messages } from '../Messages/Messages'; 10 | import { Schemas } from '../Schemas/Schemas'; 11 | 12 | import { ConfigInterface } from '../../config'; 13 | import { SpecificationContext, ConfigContext } from '../../contexts'; 14 | import AsyncApiErrorBoundary from '../ApplicationErrorHandler/ErrorBoundary'; 15 | 16 | interface Props { 17 | asyncapi: AsyncAPIDocumentInterface; 18 | config: ConfigInterface; 19 | } 20 | 21 | const AsyncApiLayout: React.FunctionComponent = ({ 22 | asyncapi, 23 | config, 24 | }) => { 25 | const [observerClassName, setObserverClassName] = useState('container:xl'); 26 | 27 | const { ref } = useResizeObserver({ 28 | onResize: ({ width }) => { 29 | requestAnimationFrame(() => { 30 | if (width === undefined) { 31 | return; 32 | } 33 | 34 | const possibleClassName = 35 | width <= 1536 ? 'container:xl' : 'container:base'; 36 | if (possibleClassName !== observerClassName) { 37 | setObserverClassName(possibleClassName); 38 | } 39 | }); 40 | }, 41 | }); 42 | 43 | const configShow = config.show ?? {}; 44 | return ( 45 | 46 | 47 |
48 | 49 |
54 | {configShow.sidebar && } 55 |
56 |
57 | {configShow.info && } 58 | {configShow.servers && } 59 | {configShow.operations && } 60 | {configShow.messages && } 61 | {configShow.schemas && } 62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default AsyncApiLayout; 74 | -------------------------------------------------------------------------------- /library/src/containers/AsyncApi/Standalone.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AsyncAPIDocumentInterface } from '@asyncapi/parser'; 3 | 4 | import { SpecificationHelpers } from '../../helpers'; 5 | import { ErrorObject, PropsSchema } from '../../types'; 6 | import { ConfigInterface, defaultConfig } from '../../config'; 7 | 8 | import AsyncApiLayout from './Layout'; 9 | import { Error } from '../Error/Error'; 10 | 11 | export interface AsyncApiProps { 12 | schema: PropsSchema; 13 | config?: Partial; 14 | error?: ErrorObject; 15 | } 16 | 17 | interface AsyncAPIState { 18 | asyncapi?: AsyncAPIDocumentInterface; 19 | error?: ErrorObject; 20 | } 21 | 22 | class AsyncApiComponent extends Component { 23 | state: AsyncAPIState = { 24 | asyncapi: undefined, 25 | error: undefined, 26 | }; 27 | 28 | constructor(props: AsyncApiProps) { 29 | super(props); 30 | 31 | const parsedSpec = SpecificationHelpers.retrieveParsedSpec(props.schema); 32 | if (parsedSpec) { 33 | this.state = { asyncapi: parsedSpec }; 34 | } 35 | } 36 | 37 | componentDidMount() { 38 | if (!this.state.asyncapi) { 39 | this.updateState(this.props.schema); 40 | } 41 | } 42 | 43 | componentDidUpdate(prevProps: AsyncApiProps) { 44 | const oldSchema = prevProps.schema; 45 | const newSchema = this.props.schema; 46 | 47 | if (oldSchema !== newSchema) { 48 | this.updateState(newSchema); 49 | } 50 | } 51 | 52 | render() { 53 | const { config, error: propError } = this.props; 54 | const { asyncapi, error: stateError } = this.state; 55 | 56 | const error = propError ?? stateError; 57 | const concatenatedConfig: ConfigInterface = { 58 | ...defaultConfig, 59 | ...config, 60 | show: { 61 | ...defaultConfig.show, 62 | ...(!!config && config.show), 63 | }, 64 | expand: { 65 | ...defaultConfig.expand, 66 | ...(!!config && config.expand), 67 | }, 68 | sidebar: { 69 | ...defaultConfig.sidebar, 70 | ...(!!config && config.sidebar), 71 | }, 72 | extensions: { 73 | ...defaultConfig.extensions, 74 | ...(!!config && config.extensions), 75 | }, 76 | }; 77 | 78 | if (!asyncapi) { 79 | if (!error) { 80 | return null; 81 | } 82 | return ( 83 | concatenatedConfig.show?.errors && ( 84 |
85 | 86 |
87 | ) 88 | ); 89 | } 90 | 91 | return ; 92 | } 93 | 94 | private updateState(schema: PropsSchema) { 95 | const parsedSpec = SpecificationHelpers.retrieveParsedSpec(schema); 96 | if (!parsedSpec) { 97 | this.setState({ asyncapi: undefined }); 98 | return; 99 | } 100 | this.setState({ asyncapi: parsedSpec }); 101 | } 102 | } 103 | 104 | export default AsyncApiComponent; 105 | -------------------------------------------------------------------------------- /library/src/containers/Error/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ERROR_TEXT } from '../../constants'; 4 | import { ErrorObject, ValidationError } from '../../types'; 5 | 6 | const renderErrors = (errors: ValidationError[]): React.ReactNode => { 7 | if (!errors) { 8 | return null; 9 | } 10 | 11 | return errors 12 | .map((singleError: ValidationError, index: number) => { 13 | if (!singleError?.title) { 14 | return null; 15 | } 16 | return ( 17 |
18 | {(singleError?.location?.startLine ?? 19 | singleError?.location?.startOffset) && ( 20 | {`line ${singleError?.location?.startLine + singleError?.location?.startOffset}:`} 21 | )} 22 | 23 | {singleError.title} 24 | 25 |
26 | ); 27 | }) 28 | .filter(Boolean); 29 | }; 30 | 31 | interface Props { 32 | error: ErrorObject; 33 | } 34 | 35 | export const Error: React.FunctionComponent = ({ error }) => { 36 | if (!error) { 37 | return null; 38 | } 39 | const { title, validationErrors } = error; 40 | 41 | return ( 42 |
43 |
44 |
45 |

46 | {title ? `${ERROR_TEXT}: ${title}` : ERROR_TEXT} 47 |

48 | {validationErrors?.length ? ( 49 |
50 |
{renderErrors(validationErrors)}
51 |
52 | ) : null} 53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /library/src/containers/Messages/MessageExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { MessageInterface, SchemaInterface } from '@asyncapi/parser'; 3 | 4 | import { CollapseButton, JSONSnippet } from '../../components'; 5 | import { MessageHelpers } from '../../helpers/message'; 6 | import { MessageExample as MessageExampleType } from '../../types'; 7 | import { useConfig } from '../../contexts'; 8 | 9 | interface Props { 10 | message: MessageInterface; 11 | } 12 | 13 | export const MessageExample: React.FunctionComponent = ({ message }) => { 14 | if (!message) { 15 | return null; 16 | } 17 | 18 | const payload = message.payload(); 19 | const headers = message.headers(); 20 | 21 | return ( 22 |
23 |

Examples

24 | {payload && ( 25 | 30 | )} 31 | {headers && ( 32 | 37 | )} 38 |
39 | ); 40 | }; 41 | 42 | interface ExampleProps { 43 | type: 'Payload' | 'Headers'; 44 | schema: SchemaInterface; 45 | examples?: MessageExampleType[]; 46 | } 47 | 48 | export const Example: React.FunctionComponent = ({ 49 | type = 'Payload', 50 | schema, 51 | examples = [], 52 | }) => { 53 | const config = useConfig(); 54 | const [expanded, setExpanded] = useState( 55 | config?.expand?.messageExamples ?? false, 56 | ); 57 | 58 | useEffect(() => { 59 | setExpanded(config?.expand?.messageExamples ?? false); 60 | // eslint-disable-next-line react-hooks/exhaustive-deps 61 | }, [config.expand]); 62 | 63 | return ( 64 |
65 |
66 | setExpanded((prev) => !prev)} 68 | expanded={expanded} 69 | chevronProps={{ 70 | className: 'fill-current text-gray-200', 71 | }} 72 | > 73 | 74 | {type} 75 | 76 | 77 |
78 |
79 | {examples && examples.length > 0 ? ( 80 |
    81 | {examples.map((example, idx) => ( 82 |
  • 83 |
    84 | {example.name 85 | ? `#${idx + 1} Example - ${example.name}` 86 | : `#${idx + 1} Example`} 87 |
    88 | {example.summary && ( 89 |

    90 | {example.summary} 91 |

    92 | )} 93 |
    94 | 98 |
    99 |
  • 100 | ))} 101 |
102 | ) : ( 103 |
104 | 108 |
109 | This example has been generated automatically. 110 |
111 |
112 | )} 113 |
114 |
115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /library/src/containers/Messages/Messages.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Message } from './Message'; 4 | 5 | import { useConfig, useSpec } from '../../contexts'; 6 | import { CommonHelpers } from '../../helpers'; 7 | import { MESSAGES_TEXT } from '../../constants'; 8 | 9 | export const Messages: React.FunctionComponent = () => { 10 | const asyncapi = useSpec(); 11 | const config = useConfig(); 12 | const messages = 13 | !asyncapi.components().isEmpty() && asyncapi.components().messages().all(); 14 | 15 | if (!messages || messages.length === 0) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
24 |

25 | {MESSAGES_TEXT} 26 |

27 |
    28 | {messages.map((message, idx) => ( 29 |
  • 34 | 41 |
  • 42 | ))} 43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /library/src/containers/Operations/Operations.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Operation } from './Operation'; 3 | import { useConfig, useSpec } from '../../contexts'; 4 | import { CommonHelpers } from '../../helpers'; 5 | import { OPERATIONS_TEXT } from '../../constants'; 6 | 7 | export const Operations: React.FunctionComponent = () => { 8 | const operations = useSpec().operations().all(); 9 | const config = useConfig(); 10 | 11 | if (!Object.keys(operations).length) { 12 | return null; 13 | } 14 | 15 | const operationsList: React.ReactNodeArray = operations.map((operation) => { 16 | const channel = operation.channels().all()[0]; 17 | const channelAddress = channel?.address() ?? ''; 18 | const operationId = CommonHelpers.getOperationIdentifier({ 19 | operation, 20 | config, 21 | }); 22 | const type = CommonHelpers.getOperationType(operation); 23 | return ( 24 |
  • 25 | 31 |
  • 32 | ); 33 | }); 34 | return ( 35 |
    39 |

    40 | {OPERATIONS_TEXT} 41 |

    42 |
      {operationsList}
    43 |
    44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /library/src/containers/Schemas/Schema.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SchemaInterface } from '@asyncapi/parser'; 3 | 4 | import { Schema as SchemaComponent } from '../../components'; 5 | 6 | interface Props { 7 | schemaName: string; 8 | schema: SchemaInterface; 9 | } 10 | 11 | export const Schema: React.FunctionComponent = ({ 12 | schemaName, 13 | schema, 14 | }) => { 15 | if (!schema) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
    21 |
    22 |
    23 | 24 |
    25 |
    26 | 27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /library/src/containers/Schemas/Schemas.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Schema } from './Schema'; 4 | 5 | import { useConfig, useSpec } from '../../contexts'; 6 | import { CommonHelpers } from '../../helpers'; 7 | import { SCHEMAS_TEXT } from '../../constants'; 8 | 9 | export const Schemas: React.FunctionComponent = () => { 10 | const asyncapi = useSpec(); 11 | const config = useConfig(); 12 | const schemas = 13 | !asyncapi.components().isEmpty() && asyncapi.components().schemas().all(); 14 | 15 | if (!schemas || schemas.length === 0) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
    24 |

    25 | {SCHEMAS_TEXT} 26 |

    27 |
      28 | {schemas.map((schema) => ( 29 |
    • 34 | 35 |
    • 36 | ))} 37 |
    38 |
    39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /library/src/containers/Servers/Server.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ServerInterface } from '@asyncapi/parser'; 3 | 4 | import { Security } from './Security'; 5 | import { Markdown, Schema, Bindings, Tags, Extensions } from '../../components'; 6 | 7 | import { useConfig } from '../../contexts'; 8 | import { CommonHelpers, SchemaHelpers } from '../../helpers'; 9 | 10 | interface Props { 11 | serverName: string; 12 | server: ServerInterface; 13 | } 14 | 15 | export const Server: React.FunctionComponent = ({ 16 | serverName, 17 | server, 18 | }) => { 19 | const config = useConfig(); 20 | 21 | if (!server) { 22 | return null; 23 | } 24 | 25 | const urlVariables = SchemaHelpers.serverVariablesToSchema( 26 | server.variables(), 27 | ); 28 | const protocolVersion = server.protocolVersion(); 29 | const security = server.security(); 30 | 31 | return ( 32 |
    33 |
    34 |
    35 |
    36 | {server.url()} 37 | 38 | {protocolVersion 39 | ? `${server.protocol()} ${protocolVersion}` 40 | : server.protocol()} 41 | 42 | 43 | {serverName} 44 | 45 |
    46 | 47 | {server.hasDescription() && ( 48 |
    49 | {server.description()} 50 |
    51 | )} 52 | 53 | {urlVariables && ( 54 |
    61 | 66 |
    67 | )} 68 | 69 | { 70 |
    76 | 77 |
    78 | } 79 | 80 | {server.bindings() && ( 81 |
    82 | 86 |
    87 | )} 88 | 89 | 90 | 91 | {server.tags().length > 0 && ( 92 |
    93 | 94 |
    95 | )} 96 |
    97 |
    98 | 99 |
    100 |
    101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /library/src/containers/Servers/Servers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Server } from './Server'; 4 | 5 | import { useConfig, useSpec } from '../../contexts'; 6 | import { CommonHelpers } from '../../helpers'; 7 | import { SERVERS_TEXT } from '../../constants'; 8 | 9 | export const Servers: React.FunctionComponent = () => { 10 | const servers = useSpec().servers().all(); 11 | const config = useConfig(); 12 | 13 | if (!servers.length) { 14 | return null; 15 | } 16 | 17 | return ( 18 |
    22 |

    23 | {SERVERS_TEXT} 24 |

    25 |
      26 | {servers.map((server) => { 27 | const serverName = server.id(); 28 | return ( 29 |
    • 37 | 42 |
    • 43 | ); 44 | })} 45 |
    46 |
    47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /library/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useConfig'; 2 | export * from './useSpec'; 3 | -------------------------------------------------------------------------------- /library/src/contexts/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { ConfigInterface } from '../config'; 3 | 4 | export const ConfigContext = createContext({}); 5 | 6 | export function useConfig() { 7 | return useContext(ConfigContext); 8 | } 9 | -------------------------------------------------------------------------------- /library/src/contexts/useSpec.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { AsyncAPIDocumentInterface } from '@asyncapi/parser'; 3 | 4 | export const SpecificationContext = 5 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any 6 | React.createContext(null as any); 7 | 8 | export function useSpec() { 9 | return useContext(SpecificationContext); 10 | } 11 | -------------------------------------------------------------------------------- /library/src/helpers/__tests__/__snapshots__/schema.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SchemaHelpers .applicatorSchemaName should not render title because title is null 1`] = `"first case:"`; 4 | 5 | exports[`SchemaHelpers .applicatorSchemaName should not render title because title is null 2`] = `"other cases:"`; 6 | 7 | exports[`SchemaHelpers .applicatorSchemaName should not render title because title is undefined 1`] = `"first case:"`; 8 | 9 | exports[`SchemaHelpers .applicatorSchemaName should not render title because title is undefined 2`] = `"other cases:"`; 10 | 11 | exports[`SchemaHelpers .applicatorSchemaName should render title 1`] = `"first case title example:"`; 12 | 13 | exports[`SchemaHelpers .applicatorSchemaName should render title 2`] = `"other cases title example:"`; 14 | -------------------------------------------------------------------------------- /library/src/helpers/__tests__/common.test.ts: -------------------------------------------------------------------------------- 1 | import { CommonHelpers } from '../common'; 2 | 3 | describe('CommonHelpers', () => { 4 | describe('.getIdentifier', () => { 5 | test('should return identifier without config argument', () => { 6 | const result = CommonHelpers.getIdentifier('test'); 7 | expect(result).toEqual(`test`); 8 | }); 9 | 10 | test('should return identifier with config argument', () => { 11 | const result = CommonHelpers.getIdentifier('test', { 12 | schemaID: 'prefix-id', 13 | }); 14 | expect(result).toEqual(`prefix-id-test`); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /library/src/helpers/__tests__/marked.test.ts: -------------------------------------------------------------------------------- 1 | import { renderMarkdown } from '../marked'; 2 | 3 | describe('marked', () => { 4 | test('should render simple markdown', () => { 5 | const result = renderMarkdown('text'); 6 | expect(result).toEqual(`

    text

    \n`); 7 | }); 8 | 9 | test('should render complex markdown', () => { 10 | const result = renderMarkdown(` 11 | # heading 12 | 13 | paragraph 14 | 15 | **bold** 16 | `); 17 | expect(result).toEqual( 18 | `

    heading

    \n

    paragraph

    \n

    bold

    \n`, 19 | ); 20 | }); 21 | 22 | test('should render code blocks with highlights', () => { 23 | const result = renderMarkdown(` 24 | \`\`\`json 25 | {"foo": "bar"} 26 | \`\`\` 27 | `); 28 | expect(result).toEqual( 29 | `
    {"foo": "bar"}\n
    \n`, 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /library/src/helpers/__tests__/sidebar.test.ts: -------------------------------------------------------------------------------- 1 | import { OperationInterface, TagV2, TagsV2 } from '@asyncapi/parser'; 2 | import { TagObject, filterObjectsByTags } from '../sidebar'; 3 | 4 | describe('sidebar', () => { 5 | describe('.filterObjectsByTags', () => { 6 | test('should handle empty objects and find nothing', () => { 7 | const tagsToFind = ['test']; 8 | const objects: TagObject[] = []; 9 | const filteredTags = filterObjectsByTags(tagsToFind, objects); 10 | expect(filteredTags.tagged.size).toEqual(0); 11 | expect(filteredTags.untagged.length).toEqual(0); 12 | }); 13 | test('should handle find one instance', () => { 14 | const tagsToFind = ['test']; 15 | const tagsToSearch = new TagsV2([new TagV2({ name: 'test' })]); 16 | const objects: TagObject[] = [ 17 | { data: {}, name: '', tags: tagsToSearch }, 18 | ]; 19 | const filteredTags = filterObjectsByTags(tagsToFind, objects); 20 | expect(filteredTags.tagged.size).toEqual(1); 21 | expect(filteredTags.untagged.length).toEqual(0); 22 | }); 23 | test('should handle find multiple instances', () => { 24 | const tagsToFind = ['test']; 25 | const obj1 = { 26 | data: {}, 27 | name: '', 28 | tags: new TagsV2([new TagV2({ name: 'test' })]), 29 | }; 30 | const obj2 = { 31 | data: {}, 32 | name: '', 33 | tags: new TagsV2([new TagV2({ name: 'none' })]), 34 | }; 35 | const obj3 = { 36 | data: {}, 37 | name: '', 38 | tags: new TagsV2([new TagV2({ name: 'test' })]), 39 | }; 40 | const objects: TagObject[] = [obj1, obj2, obj3]; 41 | const filteredTags = filterObjectsByTags(tagsToFind, objects); 42 | expect(filteredTags.tagged.size).toEqual(1); 43 | expect(filteredTags.tagged.get('test')?.length).toEqual(2); 44 | expect(filteredTags.untagged.length).toEqual(1); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /library/src/helpers/bindingsHelpers.ts: -------------------------------------------------------------------------------- 1 | class BindingsHelper { 2 | /** 3 | * 4 | * Since we do not have a reliable way to identify schema objects via a spec, schema or similar - using a list of known 5 | * binding properties of type SchemaObject 6 | * Change it when AsyncAPI will support JSON Schema specification (definition) for bindings 7 | */ 8 | private schemaObjectKeys: string[] = [ 9 | 'http.query', 10 | 'kafka.groupId', 11 | 'kafka.clientId', 12 | 'kafka.key', 13 | 'ws.headers', 14 | 'ws.query', 15 | ]; 16 | 17 | isSchemaObject(context: string, bindingType: string): boolean { 18 | return this.schemaObjectKeys.includes(`${bindingType}.${context}`); 19 | } 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | isObject(value: any): boolean { 23 | return !!value && typeof value === 'object'; 24 | } 25 | } 26 | export const bindingsHelper = new BindingsHelper(); 27 | -------------------------------------------------------------------------------- /library/src/helpers/common.ts: -------------------------------------------------------------------------------- 1 | import { ConfigInterface } from '../config'; 2 | import { OperationInterface } from '@asyncapi/parser'; 3 | import { PayloadType } from '../types'; 4 | import { 5 | PUBLISH_LABEL_DEFAULT_TEXT, 6 | RECEIVE_TEXT_LABEL_DEFAULT_TEXT, 7 | REPLIER_LABEL_DEFAULT_TEXT, 8 | REQUEST_LABEL_DEFAULT_TEXT, 9 | SEND_LABEL_DEFAULT_TEXT, 10 | SUBSCRIBE_LABEL_DEFAULT_TEXT, 11 | } from '../constants'; 12 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 13 | export class CommonHelpers { 14 | static getIdentifier(id: string, config?: ConfigInterface) { 15 | const schemaID = config?.schemaID; 16 | if (schemaID) { 17 | return `${schemaID}-${id}`; 18 | } 19 | return id; 20 | } 21 | static getOperationType(operation: OperationInterface) { 22 | if (operation.isSend()) { 23 | if (operation.reply() !== undefined) { 24 | return PayloadType.REQUEST; 25 | } else { 26 | return PayloadType.SEND; 27 | } 28 | } 29 | if (operation.isReceive() && operation.reply() !== undefined) { 30 | return PayloadType.REPLY; 31 | } 32 | return PayloadType.RECEIVE; 33 | } 34 | static getOperationIdentifier({ 35 | operation, 36 | config, 37 | }: { 38 | operation: OperationInterface; 39 | config: ConfigInterface; 40 | }) { 41 | if (operation.isSend()) { 42 | if (operation.reply() !== undefined) { 43 | return CommonHelpers.getIdentifier( 44 | `operation-${PayloadType.REQUEST}-${operation.id()}`, 45 | config, 46 | ); 47 | } else { 48 | return CommonHelpers.getIdentifier( 49 | `operation-${PayloadType.SEND}-${operation.id()}`, 50 | config, 51 | ); 52 | } 53 | } 54 | if (operation.isReceive() && operation.reply() !== undefined) { 55 | return CommonHelpers.getIdentifier( 56 | `operation-${PayloadType.REPLY}-${operation.id()}`, 57 | config, 58 | ); 59 | } 60 | return CommonHelpers.getIdentifier( 61 | `operation-${PayloadType.RECEIVE}-${operation.id()}`, 62 | config, 63 | ); 64 | } 65 | static getOperationDesignInformation({ 66 | type, 67 | config, 68 | isAsyncAPIv2, 69 | }: { 70 | type: PayloadType; 71 | config: ConfigInterface; 72 | isAsyncAPIv2: boolean; 73 | }): { borderColor: string; typeLabel: string; backgroundColor: string } { 74 | if (type === PayloadType.RECEIVE) { 75 | return { 76 | borderColor: 'border-green-600 text-green-600', 77 | backgroundColor: 'bg-green-600', 78 | typeLabel: !isAsyncAPIv2 79 | ? config.receiveLabel ?? RECEIVE_TEXT_LABEL_DEFAULT_TEXT 80 | : config.publishLabel ?? PUBLISH_LABEL_DEFAULT_TEXT, 81 | }; 82 | } 83 | if (type === PayloadType.REPLY) { 84 | return { 85 | borderColor: 'border-orange-600 text-orange-600', 86 | backgroundColor: 'bg-orange-600', 87 | typeLabel: config.replyLabel ?? REPLIER_LABEL_DEFAULT_TEXT, 88 | }; 89 | } 90 | if (type === PayloadType.REQUEST) { 91 | return { 92 | borderColor: 'border-red-600 text-red-600', 93 | backgroundColor: 'bg-red-600', 94 | typeLabel: config.requestLabel ?? REQUEST_LABEL_DEFAULT_TEXT, 95 | }; 96 | } 97 | return { 98 | borderColor: 'border-blue-600 text-blue-500', 99 | backgroundColor: 'bg-blue-600', 100 | typeLabel: !isAsyncAPIv2 101 | ? config.sendLabel ?? SEND_LABEL_DEFAULT_TEXT 102 | : config.subscribeLabel ?? SUBSCRIBE_LABEL_DEFAULT_TEXT, 103 | }; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /library/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './marked'; 3 | export * from './parser'; 4 | export * from './specification'; 5 | export * from './server'; 6 | export * from './message'; 7 | export * from './schema'; 8 | -------------------------------------------------------------------------------- /library/src/helpers/marked.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked'; 2 | 3 | // @ts-expect-error no types exists 4 | import hljs from 'highlight.js/lib/core'; 5 | 6 | // @ts-expect-error no types exists 7 | import json from 'highlight.js/lib/languages/json'; 8 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 9 | hljs.registerLanguage('json', json); 10 | 11 | // @ts-expect-error no types exists 12 | import yaml from 'highlight.js/lib/languages/yaml'; 13 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 14 | hljs.registerLanguage('yaml', yaml); 15 | 16 | // @ts-expect-error no types exists 17 | import bash from 'highlight.js/lib/languages/bash'; 18 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 19 | hljs.registerLanguage('bash', bash); 20 | 21 | const markedOptions: marked.MarkedOptions = { 22 | langPrefix: 'hljs language-', 23 | highlight: (code, language) => { 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 25 | if (!hljs.getLanguage(language)) { 26 | return code; 27 | } 28 | try { 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 30 | return hljs.highlight(code, { language }).value; 31 | } catch (e) { 32 | return code; 33 | } 34 | }, 35 | }; 36 | 37 | export function renderMarkdown(content: string): string { 38 | return marked(content, markedOptions); 39 | } 40 | 41 | export { hljs }; 42 | -------------------------------------------------------------------------------- /library/src/helpers/message.ts: -------------------------------------------------------------------------------- 1 | import { MessageInterface } from '@asyncapi/parser'; 2 | import { sample } from 'openapi-sampler'; 3 | 4 | import { MessageExample } from '../types'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 7 | export class MessageHelpers { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | static generateExample(schema: any, options: any = {}) { 10 | try { 11 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument 12 | return this.sanitizeExample(sample(schema, options)) || ''; 13 | } catch (e) { 14 | return ''; 15 | } 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | static sanitizeExample(schema: any): any { 20 | if (typeof schema === 'object' && schema && !Array.isArray(schema)) { 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 22 | return Object.entries(schema).reduce( 23 | (obj, [propertyName, property]) => { 24 | if ( 25 | !propertyName.startsWith('x-parser-') && 26 | !propertyName.startsWith('x-schema-private-') 27 | ) { 28 | obj[propertyName] = this.sanitizeExample(property); 29 | } 30 | return obj; 31 | }, 32 | {} as Record, 33 | ); 34 | } 35 | return schema; 36 | } 37 | 38 | static getPayloadExamples( 39 | msg: MessageInterface, 40 | ): MessageExample[] | undefined { 41 | const examples = msg.examples().all(); 42 | 43 | if (examples.some((e) => e.hasPayload())) { 44 | const messageExamples = examples 45 | .flatMap((e) => { 46 | if (!e.payload()) { 47 | return; 48 | } 49 | return { 50 | name: e.name(), 51 | summary: e.summary(), 52 | example: e.payload(), 53 | }; 54 | }) 55 | .filter(Boolean) as MessageExample[]; 56 | 57 | if (messageExamples.length > 0) { 58 | return messageExamples; 59 | } 60 | } 61 | 62 | const payload = msg.payload(); 63 | if (payload?.examples()) { 64 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 65 | return payload.examples()?.map((example) => ({ example })); 66 | } 67 | 68 | return undefined; 69 | } 70 | 71 | static getHeadersExamples( 72 | msg: MessageInterface, 73 | ): MessageExample[] | undefined { 74 | const examples = msg.examples().all(); 75 | if (examples.some((e) => e.hasHeaders())) { 76 | const messageExamples = examples 77 | .flatMap((e) => { 78 | if (!e.hasHeaders()) { 79 | return; 80 | } 81 | return { 82 | name: e.name(), 83 | summary: e.summary(), 84 | example: e.headers(), 85 | }; 86 | }) 87 | .filter(Boolean) as MessageExample[]; 88 | 89 | if (messageExamples.length > 0) { 90 | return messageExamples; 91 | } 92 | } 93 | 94 | const headers = msg.headers(); 95 | if (headers?.examples()) { 96 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 97 | return headers.examples()?.map((example) => ({ example })); 98 | } 99 | return undefined; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /library/src/helpers/server.ts: -------------------------------------------------------------------------------- 1 | import { SecuritySchemeInterface } from '@asyncapi/parser'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 4 | export class ServerHelpers { 5 | static securityType(value: string) { 6 | switch (value) { 7 | case 'apiKey': 8 | return 'API key'; 9 | case 'oauth2': 10 | return 'OAuth2'; 11 | case 'openIdConnect': 12 | return 'Open ID'; 13 | case 'http': 14 | return 'HTTP'; 15 | case 'userPassword': 16 | return 'User/Password'; 17 | case 'X509': 18 | return 'X509:'; 19 | case 'symmetricEncryption': 20 | return 'Symmetric Encription'; 21 | case 'asymmetricEncryption': 22 | return 'Asymmetric Encription'; 23 | case 'httpApiKey': 24 | return 'HTTP API key'; 25 | case 'scramSha256': 26 | return 'ScramSha256'; 27 | case 'scramSha512': 28 | return 'ScramSha512'; 29 | case 'gssapi': 30 | return 'GSSAPI'; 31 | default: 32 | return 'API key'; 33 | } 34 | } 35 | 36 | static flowName(value: string) { 37 | switch (value) { 38 | case 'implicit': 39 | return 'Implicit'; 40 | case 'password': 41 | return 'Password'; 42 | case 'clientCredentials': 43 | return 'Client credentials'; 44 | case 'authorizationCode': 45 | return 'Authorization Code'; 46 | default: 47 | return 'Implicit'; 48 | } 49 | } 50 | 51 | static getKafkaSecurity( 52 | protocol: string, 53 | securitySchema: SecuritySchemeInterface | null, 54 | ) { 55 | let securityProtocol; 56 | let saslMechanism; 57 | if (protocol === 'kafka') { 58 | if (securitySchema) { 59 | securityProtocol = 'SASL_PLAINTEXT'; 60 | } else { 61 | securityProtocol = 'PLAINTEXT'; 62 | } 63 | } else if (securitySchema) { 64 | securityProtocol = 'SASL_SSL'; 65 | } else { 66 | securityProtocol = 'SSL'; 67 | } 68 | 69 | if (securitySchema) { 70 | switch (securitySchema.type()) { 71 | case 'plain': 72 | saslMechanism = 'PLAIN'; 73 | break; 74 | case 'scramSha256': 75 | saslMechanism = 'SCRAM-SHA-256'; 76 | break; 77 | case 'scramSha512': 78 | saslMechanism = 'SCRAM-SHA-512'; 79 | break; 80 | case 'oauth2': 81 | saslMechanism = 'OAUTHBEARER'; 82 | break; 83 | case 'gssapi': 84 | saslMechanism = 'GSSAPI'; 85 | break; 86 | case 'X509': 87 | securityProtocol = 'SSL'; 88 | break; 89 | } 90 | } 91 | 92 | return { securityProtocol, saslMechanism }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /library/src/helpers/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { TagsInterface } from '@asyncapi/parser'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export interface TagObject { 5 | name: string; 6 | tags: TagsInterface; 7 | data: T; 8 | } 9 | export interface SortedReturnType { 10 | tagged: Map; 11 | untagged: TagObject[]; 12 | } 13 | 14 | /** 15 | * Filter an array of objects by certain tags 16 | */ 17 | export function filterObjectsByTags( 18 | tags: string[], 19 | objects: TagObject[], 20 | ): SortedReturnType { 21 | const taggedObjects = new Set(); 22 | const tagged = new Map(); 23 | tags.forEach((tag) => { 24 | const taggedForTag: TagObject[] = []; 25 | objects.forEach((obj) => { 26 | const objTags = obj.tags; 27 | const nameTags = (objTags.all() ?? []).map((t) => t.name()); 28 | const hasTag = nameTags.includes(tag); 29 | if (hasTag) { 30 | taggedForTag.push(obj); 31 | taggedObjects.add(obj); 32 | } 33 | }); 34 | if (taggedForTag.length > 0) { 35 | tagged.set(tag, taggedForTag); 36 | } 37 | }); 38 | 39 | const untagged: TagObject[] = []; 40 | objects.forEach((obj) => { 41 | if (!taggedObjects.has(obj)) { 42 | untagged.push(obj); 43 | } 44 | }); 45 | 46 | return { tagged, untagged }; 47 | } 48 | -------------------------------------------------------------------------------- /library/src/helpers/specification.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncAPIDocumentInterface, 3 | TagInterface, 4 | isAsyncAPIDocument, 5 | isOldAsyncAPIDocument, 6 | toAsyncAPIDocument, 7 | unstringify, 8 | } from '@asyncapi/parser'; 9 | import { isStringifiedDocument } from '@asyncapi/parser/cjs/document'; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 12 | export class SpecificationHelpers { 13 | /** 14 | * Returns parsed AsyncAPI specification. 15 | */ 16 | static retrieveParsedSpec( 17 | schema: unknown, 18 | ): AsyncAPIDocumentInterface | undefined { 19 | if (!schema) { 20 | return undefined; 21 | } 22 | 23 | if (isAsyncAPIDocument(schema)) { 24 | return schema; 25 | } 26 | 27 | if (isOldAsyncAPIDocument(schema)) { 28 | // Is from old parser 29 | const parsedJSON = schema.json(); 30 | return toAsyncAPIDocument(parsedJSON); 31 | } 32 | 33 | // check if input is a string and try parse it 34 | if (typeof schema === 'string') { 35 | try { 36 | schema = JSON.parse(schema) as Record; 37 | } catch (e) { 38 | return undefined; 39 | } 40 | } 41 | 42 | // at the end check if schema is a parsed JS object (as output from AsyncAPI Parser) 43 | if (isStringifiedDocument(schema)) { 44 | return unstringify(schema); 45 | } 46 | 47 | return toAsyncAPIDocument(schema); 48 | } 49 | 50 | /** 51 | * Check if given schema have one of the specified tags. 52 | */ 53 | static containTags( 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | schema: any, 56 | tags: TagInterface | TagInterface[], 57 | ): boolean { 58 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 59 | const tagsToCheck = 60 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 61 | typeof schema.tags === 'function' ? schema.tags() : undefined; 62 | if (tagsToCheck === undefined || !Array.isArray(tagsToCheck)) { 63 | return false; 64 | } 65 | const tagsArr = Array.isArray(tags) ? tags : [tags]; 66 | return tagsToCheck.some((tag: TagInterface) => 67 | tagsArr.some((t) => t.name() === tag.name()), 68 | ); 69 | } 70 | 71 | /** 72 | * Return all tags from operations 73 | */ 74 | static operationsTags(spec: AsyncAPIDocumentInterface) { 75 | const tags = new Map(); 76 | Object.entries(spec.operations().all()).forEach(([, operation]) => { 77 | if (operation?.tags().length > 0) { 78 | operation 79 | .tags() 80 | .all() 81 | .forEach((tag) => tags.set(tag.name(), tag)); 82 | } 83 | }); 84 | return Array.from(tags.values()); 85 | } 86 | 87 | /** 88 | * Return all tags from servers 89 | */ 90 | static serversTags(spec: AsyncAPIDocumentInterface) { 91 | const tags: Record = {}; 92 | Object.entries(spec.servers()).forEach(([_, server]) => { 93 | if (server.tags().length > 0) { 94 | server 95 | .tags() 96 | .all() 97 | .forEach((tag) => { 98 | if (tags[tag.name()]) { 99 | tags[tag.name()].push(_); 100 | } else { 101 | tags[tag.name()] = [_]; 102 | } 103 | }); 104 | } 105 | }); 106 | return tags; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /library/src/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncApiComponent from './containers/AsyncApi/AsyncApi'; 2 | import AsyncApiComponentWP from './containers/AsyncApi/Standalone'; 3 | 4 | export { AsyncApiProps } from './containers/AsyncApi/AsyncApi'; 5 | export { ConfigInterface } from './config/config'; 6 | export { FetchingSchemaInterface, ExtensionComponentProps } from './types'; 7 | 8 | import { hljs } from './helpers'; 9 | 10 | export { AsyncApiComponentWP, hljs }; 11 | export default AsyncApiComponent; 12 | -------------------------------------------------------------------------------- /library/src/standalone-codebase.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hydrateRoot, createRoot } from 'react-dom/client'; 3 | 4 | function querySelector(selector: string): Element | null { 5 | if (typeof document !== 'undefined') { 6 | return document.querySelector(selector); 7 | } 8 | return null; 9 | } 10 | 11 | /** 12 | * A factory that creates the rendering function of the given React component of any kind. 13 | * 14 | * @param {Any} component of any kind 15 | */ 16 | export function createRender< 17 | Props extends Parameters[1], 18 | >(component: Parameters[0]) { 19 | return (props: Props, container?: Element | DocumentFragment | null) => { 20 | container = container ?? querySelector('asyncapi'); 21 | 22 | if (container === null) { 23 | return; 24 | } 25 | 26 | const root = createRoot(container); 27 | 28 | root.render(React.createElement(component, props)); 29 | }; 30 | } 31 | 32 | /** 33 | * A factory that creates the hydration function of the given React component of any kind. 34 | * 35 | * @param {Any} component of any kind 36 | */ 37 | export function createHydrate< 38 | Props extends Parameters[1], 39 | >(component: Parameters[0]) { 40 | return (props: Props, container?: Element | Document | null) => { 41 | container = container ?? querySelector('asyncapi'); 42 | 43 | if (container === null) { 44 | return; 45 | } 46 | 47 | hydrateRoot(container, React.createElement(component, props)); 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /library/src/standalone-without-parser.ts: -------------------------------------------------------------------------------- 1 | import AsyncApiComponent, { 2 | AsyncApiProps, 3 | } from './containers/AsyncApi/Standalone'; 4 | 5 | import { createRender, createHydrate } from './standalone-codebase'; 6 | import { hljs } from './helpers'; 7 | 8 | // eslint-disable-next-line import/no-anonymous-default-export 9 | export default { 10 | render: createRender(AsyncApiComponent), 11 | hydrate: createHydrate(AsyncApiComponent), 12 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 13 | hljs, 14 | }; 15 | -------------------------------------------------------------------------------- /library/src/standalone.ts: -------------------------------------------------------------------------------- 1 | import AsyncApiComponent, { 2 | AsyncApiProps, 3 | } from './containers/AsyncApi/AsyncApi'; 4 | 5 | import { createRender, createHydrate } from './standalone-codebase'; 6 | import { hljs } from './helpers'; 7 | 8 | // eslint-disable-next-line import/no-anonymous-default-export 9 | export default { 10 | render: createRender(AsyncApiComponent), 11 | hydrate: createHydrate(AsyncApiComponent), 12 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 13 | hljs, 14 | }; 15 | -------------------------------------------------------------------------------- /library/src/styles/default.css: -------------------------------------------------------------------------------- 1 | @import 'highlight.js/styles/night-owl.css'; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer components { 8 | .container\:base .burger-menu { 9 | @apply lg:hidden; 10 | } 11 | 12 | .container\:base .sidebar { 13 | @apply lg:relative lg:block lg:w-64 lg:h-auto; 14 | } 15 | 16 | .container\:xl .sidebar--wrapper{ 17 | @apply xl:w-full; 18 | @apply sm:w-full; 19 | } 20 | 21 | .container\:base .sidebar--content { 22 | @apply lg:w-56; 23 | } 24 | 25 | /*.container\:xl .sidebar--content { 26 | @apply absolute; 27 | left: 50%; 28 | transform: translate(-50%, 0); 29 | }*/ 30 | 31 | .container\:base .panel-item { 32 | @apply 2xl:flex; 33 | } 34 | 35 | .container\:xl .panel-item { 36 | @apply block; 37 | } 38 | 39 | .container\:base .panel--center .panel-item--center { 40 | @apply 2xl:w-7/12; 41 | } 42 | 43 | .container\:base .panel--center .panel-item--right { 44 | @apply 2xl:w-5/12; 45 | } 46 | 47 | .container\:xl .panel--center .panel-item--center { 48 | @apply w-full; 49 | } 50 | 51 | .container\:xl .panel--center .panel-item--right { 52 | @apply w-full; 53 | } 54 | 55 | .container\:base .examples { 56 | @apply 2xl:mt-0; 57 | } 58 | 59 | .container\:base .panel--right { 60 | @apply hidden 2xl:block 2xl:w-5/12; 61 | } 62 | 63 | .container\:xl .panel--right { 64 | @apply hidden; 65 | } 66 | 67 | .prose pre { 68 | @apply whitespace-pre-wrap; 69 | } 70 | } 71 | 72 | @layer utilities { 73 | .break-anywhere { 74 | overflow-wrap: anywhere; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /library/src/types.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAPIDocumentInterface, BaseModel } from '@asyncapi/parser'; 2 | 3 | export type PropsSchema = 4 | | string 5 | | FetchingSchemaInterface 6 | | AsyncAPIDocumentInterface 7 | | object; 8 | 9 | export type NullableAsyncApi = AsyncAPIDocumentInterface | null; 10 | 11 | export interface AsyncApiState { 12 | validatedSchema: NullableAsyncApi; 13 | error?: ErrorObject; 14 | } 15 | 16 | export function isFetchingSchemaInterface( 17 | schema: PropsSchema, 18 | ): schema is FetchingSchemaInterface { 19 | return (schema as FetchingSchemaInterface).url !== undefined; 20 | } 21 | 22 | export interface FetchingSchemaInterface { 23 | url: string; 24 | requestOptions?: RequestInit; 25 | } 26 | 27 | export interface ParserReturn { 28 | asyncapi?: AsyncAPIDocumentInterface; 29 | error?: ErrorObject; 30 | } 31 | 32 | export enum PayloadType { 33 | SEND = 'send', 34 | RECEIVE = 'receive', 35 | REQUEST = 'request', 36 | REPLY = 'reply', 37 | } 38 | 39 | export interface MessageExample { 40 | name?: string; 41 | summary?: string; 42 | example: unknown; 43 | } 44 | 45 | export interface ValidationError { 46 | title: string; 47 | location?: { 48 | jsonPointer: string; 49 | startLine: number; 50 | startColumn: number; 51 | startOffset: number; 52 | endLine: number; 53 | endColumn: number; 54 | endOffset: number; 55 | }; 56 | } 57 | 58 | export interface ErrorObject { 59 | type: string; 60 | title: string; 61 | detail?: string; 62 | parsedJSON?: unknown; 63 | validationErrors?: ValidationError[]; 64 | location?: { 65 | startLine: number; 66 | startColumn: number; 67 | startOffset: number; 68 | }; 69 | refs?: { 70 | title: string; 71 | jsonPointer: string; 72 | startLine: number; 73 | startColumn: number; 74 | startOffset: number; 75 | endLine: number; 76 | endColumn: number; 77 | endOffset: number; 78 | }[]; 79 | } 80 | 81 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 82 | export interface ExtensionComponentProps { 83 | propertyName: string; 84 | propertyValue: V; 85 | document: AsyncAPIDocumentInterface; 86 | parent: BaseModel; 87 | } 88 | -------------------------------------------------------------------------------- /library/src/without-parser.ts: -------------------------------------------------------------------------------- 1 | import AsyncApiComponent from './containers/AsyncApi/Standalone'; 2 | 3 | import { hljs } from './helpers'; 4 | 5 | export { hljs }; 6 | export default AsyncApiComponent; 7 | -------------------------------------------------------------------------------- /library/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "es5", 6 | "outDir": "lib/cjs" 7 | }, 8 | "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"], 9 | "exclude": [ 10 | "src/standalone.ts", 11 | "src/standalone-*.ts", 12 | "**/*.test.ts", 13 | "**/*.test.tsx" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /library/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "target": "es5", 6 | "outDir": "lib/esm" 7 | }, 8 | "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"], 9 | "exclude": [ 10 | "src/standalone.ts", 11 | "src/standalone-*.ts", 12 | "**/*.test.ts", 13 | "**/*.test.tsx" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "target": "es5", 6 | "outDir": "lib/esm", 7 | "types": ["jest", "node"] 8 | }, 9 | "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"] 10 | } 11 | -------------------------------------------------------------------------------- /library/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "declarationMap": true, 7 | "outDir": "lib/types" 8 | }, 9 | "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"], 10 | "exclude": [ 11 | "src/standalone.ts", 12 | "src/standalone-*.ts", 13 | "**/*.test.ts", 14 | "**/*.test.tsx" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /library/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const BundleAnalyzerPlugin = 3 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 5 | 6 | const umdBundle = { 7 | entry: { 8 | index: './src/index.ts', 9 | 'without-parser': './src/without-parser.ts', 10 | }, 11 | target: 'web', 12 | mode: 'production', 13 | 14 | output: { 15 | path: path.resolve(__dirname, 'browser'), 16 | filename: '[name].js', 17 | library: 'AsyncApiComponent', 18 | libraryTarget: 'umd', 19 | libraryExport: 'default', 20 | umdNamedDefine: true, 21 | globalObject: `(typeof self !== 'undefined' ? self : this)`, 22 | }, 23 | 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | loader: 'ts-loader', 29 | exclude: /node_modules/, 30 | options: { 31 | configFile: 'tsconfig.esm.json', 32 | transpileOnly: true, 33 | }, 34 | }, 35 | ], 36 | }, 37 | resolve: { 38 | extensions: ['.ts', '.tsx', '.js'], 39 | alias: { 40 | 'nimma/legacy$': path.resolve( 41 | __dirname, 42 | '../node_modules/nimma/dist/legacy/cjs/index.js', 43 | ), 44 | 'nimma/fallbacks$': path.resolve( 45 | __dirname, 46 | '../node_modules/nimma/dist/cjs/fallbacks/index.js', 47 | ), 48 | }, 49 | fallback: { 50 | fs: false, 51 | }, 52 | }, 53 | externals: { 54 | react: { 55 | root: 'React', 56 | commonjs2: 'react', 57 | commonjs: 'react', 58 | amd: 'react', 59 | }, 60 | }, 61 | 62 | plugins: [ 63 | /** 64 | * Uncomment plugin when you wanna see dependency map of bundled package 65 | */ 66 | // new BundleAnalyzerPlugin(), 67 | new NodePolyfillPlugin(), 68 | ], 69 | }; 70 | 71 | const standaloneBundle = { 72 | entry: { 73 | index: './src/standalone.ts', 74 | 'without-parser': './src/standalone-without-parser.ts', 75 | }, 76 | target: 'web', 77 | mode: 'production', 78 | 79 | output: { 80 | path: path.resolve(__dirname, 'browser/standalone'), 81 | filename: '[name].js', 82 | library: 'AsyncApiStandalone', 83 | libraryTarget: 'umd', 84 | libraryExport: 'default', 85 | umdNamedDefine: true, 86 | globalObject: `(typeof self !== 'undefined' ? self : this)`, 87 | }, 88 | 89 | module: { 90 | rules: [ 91 | { 92 | test: /\.tsx?$/, 93 | loader: 'ts-loader', 94 | exclude: /node_modules/, 95 | options: { 96 | configFile: 'tsconfig.esm.json', 97 | transpileOnly: true, 98 | }, 99 | }, 100 | { 101 | include: [ 102 | path.resolve( 103 | __dirname, 104 | '../node_modules/nimma/dist/cjs/fallbacks/jsonpath-plus.js', 105 | ), 106 | ], 107 | use: ['remove-hashbag-loader'], 108 | }, 109 | ], 110 | }, 111 | resolveLoader: { 112 | alias: { 113 | 'remove-hashbag-loader': path.join( 114 | __dirname, 115 | './loaders/remove-hashbag-loader', 116 | ), 117 | }, 118 | }, 119 | resolve: { 120 | extensions: ['.ts', '.tsx', '.js'], 121 | alias: { 122 | 'nimma/legacy$': path.resolve( 123 | __dirname, 124 | '../node_modules/nimma/dist/legacy/cjs/index.js', 125 | ), 126 | 'nimma/fallbacks$': path.resolve( 127 | __dirname, 128 | '../node_modules/nimma/dist/cjs/fallbacks/index.js', 129 | ), 130 | }, 131 | fallback: { 132 | fs: false, 133 | }, 134 | }, 135 | 136 | plugins: [ 137 | /** 138 | * Uncomment plugin when you wanna see dependency map of bundled package 139 | */ 140 | // new BundleAnalyzerPlugin(), 141 | new NodePolyfillPlugin(), 142 | ], 143 | }; 144 | 145 | const bundles = []; 146 | 147 | process.env.BUILD_MODE === 'umd' && bundles.push(umdBundle); 148 | process.env.BUILD_MODE === 'standalone' && bundles.push(standaloneBundle); 149 | 150 | module.exports = bundles; 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "repository": { 4 | "type": "git", 5 | "url": "git+https://github.com/asyncapi/asyncapi-react.git" 6 | }, 7 | "author": { 8 | "name": "The AsyncAPI maintainers", 9 | "url": "https://www.asyncapi.com" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/asyncapi/asyncapi-react/issues" 13 | }, 14 | "engines": { 15 | "npm": ">=9.0.0", 16 | "node": ">=18.0.0" 17 | }, 18 | "workspaces": [ 19 | "library", 20 | "playground", 21 | "web-component" 22 | ], 23 | "scripts": { 24 | "clean": "lerna clean", 25 | "start": "lerna exec --parallel -- npm run start", 26 | "test": "cd library && npm test", 27 | "build": "npm run build:library && npm run build:webcomponent && npm run build:playground", 28 | "build:library": "cd library && npm run prepare", 29 | "build:webcomponent": "cd web-component && npm run bundle", 30 | "build:playground": "cd playground && npm run build", 31 | "lint": "eslint --max-warnings 0 .", 32 | "lint:fix": "eslint --max-warnings 0 . --fix && npm run format", 33 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,html,css,yaml,yml}\"", 34 | "markdownlint": "markdownlint **/*.md", 35 | "prepublishOnly": "npm run build", 36 | "gen-readme-toc": "markdown-toc -i README.md", 37 | "generate:assets": "npm run gen-readme-toc", 38 | "bump:webcomp:version": "cd web-component && npm --no-git-tag-version --allow-same-version version $VERSION", 39 | "bump:lib:version": "cd library && npm --no-git-tag-version --allow-same-version version $VERSION", 40 | "bump:version": "npm run bump:lib:version && npm run bump:webcomp:version && npm run install:reactcomp:webcomponent", 41 | "install:reactcomp:webcomponent": "cd web-component && npm run install:reactcomp", 42 | "get:name": "cd library && npm run get:name", 43 | "get:version": "cd library && npm run get:version" 44 | }, 45 | "devDependencies": { 46 | "@typescript-eslint/eslint-plugin": "^7.7.1", 47 | "@typescript-eslint/eslint-plugin-tslint": "^7.0.2", 48 | "@typescript-eslint/parser": "^7.7.1", 49 | "concurrently": "^6.0.1", 50 | "eslint": "^8.57.0", 51 | "eslint-config-next": "^14.2.2", 52 | "eslint-config-prettier": "^9.1.0", 53 | "eslint-plugin-import": "^2.29.1", 54 | "eslint-plugin-jest": "^28.2.0", 55 | "eslint-plugin-prettier": "^5.1.3", 56 | "eslint-plugin-react": "^7.34.1", 57 | "eslint-plugin-sonarjs": "^0.25.1", 58 | "lerna": "^8.1.2", 59 | "markdown-toc": "^1.2.0", 60 | "markdownlint-cli": "^0.17.0", 61 | "prettier": "^3.2.5", 62 | "tslib": "^1.10.0", 63 | "typescript": "^5.3.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm start 9 | # or 10 | yarn start 11 | # or 12 | pnpm start 13 | # or 14 | bun start 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | -------------------------------------------------------------------------------- /playground/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /playground/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Metadata } from 'next'; 3 | import { Inter } from 'next/font/google'; 4 | import './globals.css'; 5 | 6 | const inter = Inter({ subsets: ['latin'] }); 7 | 8 | export const metadata: Metadata = { 9 | title: 'AsyncAPI React Playground App', 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /playground/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import '@asyncapi/react-component/styles/default.min.css'; 3 | import React, { Component } from 'react'; 4 | import AsyncApi, { ConfigInterface } from '@asyncapi/react-component'; 5 | import { 6 | Navigation, 7 | CodeEditorComponent, 8 | FetchSchema, 9 | RefreshIcon, 10 | Tabs, 11 | Tab, 12 | PlaygroundWrapper, 13 | CodeEditorsWrapper, 14 | AsyncApiWrapper, 15 | SplitWrapper, 16 | } from '@/components'; 17 | import { defaultConfig, parse, debounce } from '@/utils'; 18 | import * as specs from '@/specs'; 19 | 20 | const defaultSchema = specs.streetlights; 21 | 22 | interface State { 23 | schema: string; 24 | config: string; 25 | schemaFromExternalResource: string; 26 | refreshing: boolean; 27 | } 28 | 29 | class Playground extends Component { 30 | updateSchemaFn: (value: string) => void; 31 | updateConfigFn: (value: string) => void; 32 | 33 | state = { 34 | schema: defaultSchema, 35 | config: defaultConfig, 36 | schemaFromExternalResource: '', 37 | refreshing: false, 38 | }; 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | constructor(props: any) { 42 | super(props); 43 | this.updateSchemaFn = debounce( 44 | this.updateSchema, 45 | 750, 46 | this.startRefreshing, 47 | this.stopRefreshing, 48 | ); 49 | this.updateConfigFn = debounce( 50 | this.updateConfig, 51 | 750, 52 | this.startRefreshing, 53 | this.stopRefreshing, 54 | ); 55 | } 56 | 57 | render() { 58 | const { schema, config, schemaFromExternalResource } = this.state; 59 | const parsedConfig = parse(config || defaultConfig); 60 | 61 | return ( 62 | 63 | 64 | 65 | 66 | 69 | {'\uE00A'} 70 | 71 | } 72 | > 73 | 74 | <> 75 | 78 | 84 | 85 | 86 | 87 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | } 102 | 103 | private updateSchema = (schema: string) => { 104 | this.setState({ schema }); 105 | }; 106 | 107 | private updateSchemaFromExternalResource = (schema: string) => { 108 | this.setState({ schemaFromExternalResource: schema }); 109 | }; 110 | 111 | private updateConfig = (config: string) => { 112 | this.setState({ config }); 113 | }; 114 | 115 | private startRefreshing = (): void => { 116 | setTimeout(() => { 117 | this.setState({ refreshing: true }); 118 | }, 500); 119 | }; 120 | 121 | private stopRefreshing = (): void => { 122 | this.setState({ refreshing: false }); 123 | }; 124 | } 125 | 126 | export default Playground; 127 | -------------------------------------------------------------------------------- /playground/components/CodeEditorComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CodeMirror from '@uiw/react-codemirror'; 3 | import { yaml } from '@codemirror/lang-yaml'; 4 | import { material } from '@uiw/codemirror-theme-material'; 5 | import { CodeEditorWrapper } from './styled'; 6 | 7 | interface Props { 8 | code: string; 9 | externalResource?: string; 10 | parentCallback(value: string): void; 11 | } 12 | 13 | interface State { 14 | code: string; 15 | } 16 | 17 | class CodeEditorComponent extends Component { 18 | state = { 19 | code: this.props.code, 20 | }; 21 | 22 | componentDidUpdate(nextProps: Props) { 23 | const { externalResource } = this.props; 24 | if (externalResource && nextProps.externalResource !== externalResource) { 25 | this.setState({ code: externalResource }); 26 | } 27 | } 28 | 29 | render() { 30 | const { 31 | state: { code }, 32 | } = this; 33 | 34 | return ( 35 | 36 | { 45 | this.props.parentCallback(value); 46 | }} 47 | /> 48 | 49 | ); 50 | } 51 | } 52 | 53 | export default CodeEditorComponent; 54 | -------------------------------------------------------------------------------- /playground/components/FetchSchema.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import React, { Component } from 'react'; 3 | import { InputWrapper, InputField, Button } from './styled'; 4 | import { fetchSchema } from '@/utils'; 5 | 6 | interface Props { 7 | parentCallback(value: string): void; 8 | } 9 | 10 | interface State { 11 | link: string; 12 | } 13 | 14 | class FetchSchema extends Component { 15 | state = { 16 | link: '', 17 | }; 18 | 19 | render() { 20 | const { link } = this.state; 21 | 22 | return ( 23 | 24 | this.setState({ link: e.target.value })} 28 | /> 29 | 32 | 33 | ); 34 | } 35 | 36 | private fetchSchemaFromExternalResources = async () => { 37 | try { 38 | new URL(this.state.link); 39 | } catch (e) { 40 | return; 41 | } 42 | const { 43 | props: { parentCallback }, 44 | state: { link }, 45 | } = this; 46 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call 47 | parentCallback((await fetchSchema(link)) as string); 48 | }; 49 | } 50 | 51 | export default FetchSchema; 52 | -------------------------------------------------------------------------------- /playground/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | NavigationWrapper, 5 | NavigationHeader, 6 | NavigationHeaderH1, 7 | NavigationHeaderIcon, 8 | NavigationHeaderAsyncApiText, 9 | NavigationHeaderEditorText, 10 | } from './styled'; 11 | 12 | const NavigationComponent = () => ( 13 | 14 | 15 | 16 | 20 | 21 | AsyncAPI React 22 | 23 | editor 24 | 25 | 26 | 27 | ); 28 | 29 | export default NavigationComponent; 30 | -------------------------------------------------------------------------------- /playground/components/SplitWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Split from 'react-split'; 3 | 4 | interface SplitWrapperProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | // react-split should be replaced with a React 18 friendly library or CSS 9 | const SplitWrapper = (props: SplitWrapperProps) => ( 10 | <> 11 | { 20 | const gutter = document.createElement('div'); 21 | gutter.onmouseover = () => (gutter.style.cursor = 'ew-resize'); 22 | return gutter; 23 | }} 24 | gutterStyle={() => ({ 25 | backgroundColor: 'gray', 26 | width: '7px', 27 | })} 28 | minSize={250} 29 | > 30 | {props.children} 31 | 32 | 33 | ); 34 | 35 | export default SplitWrapper; 36 | -------------------------------------------------------------------------------- /playground/components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { TabLink, TabWrapper } from './styled'; 4 | 5 | export interface TabProps { 6 | children: React.ReactNode; // NOSONAR 7 | title: string; 8 | tabIndex?: number; 9 | isActive?: boolean; 10 | parentCallback?: (value: number) => void; 11 | } 12 | 13 | class Tab extends Component { 14 | render() { 15 | const { title, tabIndex, isActive, parentCallback } = this.props; 16 | 17 | return ( 18 | 19 | { 21 | event.preventDefault(); 22 | if (parentCallback && tabIndex != undefined) { 23 | parentCallback(tabIndex); 24 | } 25 | }} 26 | $active={isActive} 27 | > 28 | {title} 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | export default Tab; 36 | -------------------------------------------------------------------------------- /playground/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { 4 | TabsWrapper, 5 | TabsHeader, 6 | TabsAdditionalHeaderContent, 7 | TabsContent, 8 | } from './styled'; 9 | 10 | import { TabProps } from './Tab'; 11 | 12 | interface Props { 13 | additionalHeaderContent?: React.ReactNode; 14 | defaultActiveTabIndex?: number; 15 | children: React.ReactNode; 16 | } 17 | 18 | interface State { 19 | activeTabIndex: number; 20 | } 21 | 22 | class Tabs extends Component { 23 | constructor(props: Props) { 24 | super(props); 25 | this.state = { 26 | activeTabIndex: this.props.defaultActiveTabIndex 27 | ? this.props.defaultActiveTabIndex 28 | : 0, 29 | }; 30 | } 31 | 32 | handleTabClick = (tabIndex: number) => { 33 | this.setState({ 34 | activeTabIndex: tabIndex, 35 | }); 36 | }; 37 | 38 | renderHeader = (children: React.ReactElement[]) => 39 | React.Children.map(children, (child, index) => { 40 | const c = child; 41 | return React.cloneElement(c, { 42 | title: c.props.title, 43 | parentCallback: this.handleTabClick, 44 | tabIndex: index, 45 | isActive: index === this.state.activeTabIndex, 46 | }); 47 | }); 48 | 49 | renderActiveTab = (children: React.ReactElement[]) => { 50 | if (children[this.state.activeTabIndex]) { 51 | return children[this.state.activeTabIndex].props.children; 52 | } 53 | return null; 54 | }; 55 | 56 | render() { 57 | const { additionalHeaderContent } = this.props; 58 | const children = [] 59 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any 60 | .concat(...(this.props.children as any)) 61 | .filter((child) => child !== null && child !== undefined); 62 | 63 | return ( 64 | 65 | 66 | {this.renderHeader(children)} 67 | 68 | {additionalHeaderContent} 69 | 70 | 71 | {this.renderActiveTab(children)} 72 | 73 | ); 74 | } 75 | } 76 | 77 | export default Tabs; 78 | -------------------------------------------------------------------------------- /playground/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Navigation } from './Navigation'; 2 | export { default as CodeEditorComponent } from './CodeEditorComponent'; 3 | export { default as FetchSchema } from './FetchSchema'; 4 | export { default as SplitWrapper } from './SplitWrapper'; 5 | export { default as Tabs } from './Tabs'; 6 | export { default as Tab } from './Tab'; 7 | export * from './styled'; 8 | -------------------------------------------------------------------------------- /playground/next.config.mjs: -------------------------------------------------------------------------------- 1 | const isGithubActions = process.env.GITHUB_ACTIONS || false; 2 | 3 | let assetPrefix; 4 | let basePath; 5 | 6 | if (isGithubActions) { 7 | const repo = process.env.GITHUB_REPOSITORY.replace('asyncapi/', ''); 8 | assetPrefix = `/${repo}/`; 9 | basePath = `/${repo}`; 10 | } 11 | 12 | /** @type {import('next').NextConfig} */ 13 | const nextConfig = { 14 | output: 'export', 15 | reactStrictMode: true, 16 | compiler: { 17 | styledComponents: true, 18 | }, 19 | webpack: config => { 20 | config.resolve.fallback = { 21 | fs: false, 22 | }; 23 | 24 | return config; 25 | }, 26 | assetPrefix, 27 | basePath, 28 | eslint: { 29 | ignoreDuringBuilds: true, 30 | }, 31 | }; 32 | 33 | export default nextConfig; 34 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "private": true, 4 | "scripts": { 5 | "build": "next build && touch out/.nojekyll", 6 | "start": "next dev" 7 | }, 8 | "dependencies": { 9 | "@asyncapi/react-component": "file:../library", 10 | "@codemirror/lang-yaml": "^6.0.0", 11 | "@uiw/codemirror-theme-material": "^4.21.24", 12 | "@uiw/react-codemirror": "^4.21.24", 13 | "next": "14.2.15", 14 | "react": "^18", 15 | "react-dom": "^18", 16 | "react-split": "^2.0.14", 17 | "styled-components": "^6.1.8" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "autoprefixer": "^10.0.1", 24 | "postcss": "^8", 25 | "tailwindcss": "^3.3.0", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /playground/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /playground/specs/anyOf.ts: -------------------------------------------------------------------------------- 1 | export const anyOf = ` 2 | asyncapi: '2.0.0' 3 | info: 4 | title: AnyOf example 5 | version: '1.0.0' 6 | 7 | channels: 8 | test: 9 | publish: 10 | message: 11 | $ref: '#/components/messages/testMessages' 12 | 13 | components: 14 | messages: 15 | testMessages: 16 | payload: 17 | anyOf: # anyOf in payload schema 18 | - $ref: "#/components/schemas/objectWithKey" 19 | - $ref: "#/components/schemas/objectWithKey2" 20 | 21 | schemas: 22 | objectWithKey: 23 | type: object 24 | properties: 25 | key: 26 | type: string 27 | objectWithKey2: 28 | type: object 29 | properties: 30 | key2: 31 | type: string 32 | `; 33 | -------------------------------------------------------------------------------- /playground/specs/application-headers.ts: -------------------------------------------------------------------------------- 1 | export const applicationHeaders = ` 2 | asyncapi: '2.0.0' 3 | info: 4 | title: Application Headers example 5 | version: '1.0.0' 6 | description: A cut of the Streetlights API to test application header changes supporting #112 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | servers: 12 | production: 13 | url: api.streetlights.smartylighting.com:{port} 14 | protocol: mqtt 15 | description: Test broker 16 | variables: 17 | port: 18 | description: Secure connection (TLS) is available through port 8883. 19 | default: '1883' 20 | enum: 21 | - '1883' 22 | - '8883' 23 | 24 | defaultContentType: application/json 25 | 26 | channels: 27 | smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured: 28 | parameters: 29 | streetlightId: 30 | $ref: '#/components/parameters/streetlightId' 31 | subscribe: 32 | summary: Receive information about environmental lighting conditions of a particular streetlight. 33 | operationId: receiveLightMeasurement 34 | message: 35 | $ref: '#/components/messages/lightMeasured' 36 | 37 | components: 38 | messages: 39 | lightMeasured: 40 | name: lightMeasured 41 | title: Light measured 42 | summary: Inform about environmental lighting conditions for a particular streetlight. 43 | correlationId: 44 | location: "$message.header#/MQMD/CorrelId" 45 | contentType: application/json 46 | headers: 47 | type: object 48 | properties: 49 | MQMD: 50 | type: object 51 | properties: 52 | CorrelId: 53 | type: string 54 | minLength: 24 55 | maxLength: 24 56 | format: binary 57 | applicationInstanceId: 58 | $ref: "#/components/schemas/applicationInstanceId" 59 | payload: 60 | $ref: "#/components/schemas/lightMeasuredPayload" 61 | 62 | schemas: 63 | lightMeasuredPayload: 64 | type: object 65 | properties: 66 | lumens: 67 | type: integer 68 | minimum: 0 69 | description: Light intensity measured in lumens. 70 | sentAt: 71 | $ref: "#/components/schemas/sentAt" 72 | sentAt: 73 | type: string 74 | format: date-time 75 | description: Date and time when the message was sent. 76 | applicationInstanceId: 77 | description: Unique identifier for a given instance of the publishing application 78 | type: string 79 | 80 | parameters: 81 | streetlightId: 82 | description: The ID of the streetlight. 83 | schema: 84 | type: string 85 | `; 86 | -------------------------------------------------------------------------------- /playground/specs/circular.ts: -------------------------------------------------------------------------------- 1 | export const circular = ` 2 | asyncapi: 2.2.0 3 | info: 4 | title: My Circular API 5 | version: '1.0.0' 6 | channels: 7 | recursive: 8 | subscribe: 9 | message: 10 | payload: 11 | $ref: '#/components/schemas/RecursiveSelf' 12 | nonRecursive: 13 | subscribe: 14 | message: 15 | payload: 16 | $ref: '#/components/schemas/NonRecursive' 17 | testChannel: 18 | subscribe: 19 | message: 20 | oneOf: 21 | - $ref: '#/components/messages/testMessage' 22 | components: 23 | messages: 24 | testMessage: 25 | contentType: application/json 26 | payload: 27 | $ref: '#/components/schemas/NormalSchemaA' 28 | schemas: 29 | NonRecursive: 30 | type: object 31 | properties: 32 | child: 33 | $ref: '#/components/schemas/NonRecursiveChild' 34 | NonRecursiveChild: 35 | type: object 36 | properties: 37 | value: 38 | type: string 39 | RecursiveSelf: 40 | type: object 41 | properties: 42 | selfChildren: 43 | type: array 44 | items: 45 | $ref: '#/components/schemas/RecursiveSelf' 46 | selfObjectChildren: 47 | type: object 48 | properties: 49 | test: 50 | $ref: '#/components/schemas/RecursiveSelf' 51 | nonRecursive: 52 | type: string 53 | selfSomething: 54 | type: object 55 | properties: 56 | test: 57 | $ref: '#/components/schemas/RecursiveAncestor' 58 | RecursiveAncestor: 59 | type: object 60 | properties: 61 | ancestorChildren: 62 | type: array 63 | items: 64 | $ref: '#/components/schemas/RecursiveSelf' 65 | ancestorSomething: 66 | type: string 67 | NormalSchemaA: 68 | type: object 69 | properties: 70 | schemaBReference: 71 | $ref: '#/components/schemas/NormalSchemaB' 72 | schemaCReference: 73 | $ref: '#/components/schemas/NormalSchemaC' 74 | commonEnumName: 75 | type: string 76 | enum: 77 | - ENUM_1 78 | - ENUM_2 79 | NormalSchemaB: 80 | type: string 81 | enum: 82 | - ENUM_A 83 | - ENUM_B 84 | - ENUM_C 85 | - ENUM_D 86 | NormalSchemaC: 87 | allOf: 88 | - $ref: '#/components/schemas/NormalSchemaB' 89 | - type: string 90 | enum: 91 | - ENUM_E 92 | NestedAllOfSchema: 93 | allOf: 94 | - $ref: '#/components/schemas/NormalSchemaA' 95 | - type: object 96 | properties: 97 | parent: 98 | allOf: 99 | - $ref: '#/components/schemas/NestedAllOfSchema' 100 | - $ref: '#/components/schemas/NormalSchemaA' 101 | name: 102 | type: string 103 | required: 104 | - name 105 | OneOf: 106 | type: object 107 | properties: 108 | kind: 109 | oneOf: 110 | - $ref: '#/components/schemas/OneOf' 111 | - type: string 112 | - enum: 113 | - boolean 114 | - string 115 | AnyOf: 116 | anyOf: 117 | - type: integer 118 | - type: number 119 | - type: string 120 | - type: boolean 121 | - type: object 122 | - type: array 123 | items: 124 | $ref: "#/components/schemas/AnyOf" 125 | RecursiveComplex: 126 | type: [object, array] 127 | patternProperties: 128 | ^foo: 129 | $ref: '#/components/schemas/RecursiveSelf' 130 | ^bar: 131 | type: string 132 | contains: 133 | $ref: '#/components/schemas/RecursiveComplex' 134 | items: 135 | - type: string 136 | - $ref: '#/components/schemas/RecursiveComplex' 137 | if: 138 | $ref: '#/components/schemas/RecursiveAncestor' 139 | then: 140 | $ref: '#/components/schemas/RecursiveComplex' 141 | `; 142 | -------------------------------------------------------------------------------- /playground/specs/correlation-id.ts: -------------------------------------------------------------------------------- 1 | export const correlationId = ` 2 | asyncapi: '2.0.0' 3 | info: 4 | title: Correlation ID Example 5 | version: '1.0.0' 6 | description: A cut of the Streetlights API to test Correlation ID 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | servers: 12 | production: 13 | url: api.streetlights.smartylighting.com:{port} 14 | protocol: mqtt 15 | description: Test broker 16 | variables: 17 | port: 18 | description: Secure connection (TLS) is available through port 8883. 19 | default: '1883' 20 | enum: 21 | - '1883' 22 | - '8883' 23 | security: 24 | - apiKey: [] 25 | - supportedOauthFlows: 26 | - streetlights:on 27 | - streetlights:off 28 | - streetlights:dim 29 | - openIdConnectWellKnown: [] 30 | 31 | defaultContentType: application/json 32 | 33 | channels: 34 | smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured: 35 | parameters: 36 | streetlightId: 37 | $ref: '#/components/parameters/streetlightId' 38 | subscribe: 39 | summary: Receive information about environmental lighting conditions of a particular streetlight. 40 | operationId: receiveLightMeasurement 41 | message: 42 | $ref: '#/components/messages/lightMeasured' 43 | 44 | smartylighting/streetlights/1/0/action/{streetlightId}/dim: 45 | parameters: 46 | streetlightId: 47 | $ref: '#/components/parameters/streetlightId' 48 | publish: 49 | operationId: dimLight 50 | message: 51 | $ref: '#/components/messages/dimLight' 52 | 53 | components: 54 | messages: 55 | lightMeasured: 56 | name: lightMeasured 57 | title: Light measured 58 | summary: Inform about environmental lighting conditions for a particular streetlight. 59 | correlationId: 60 | location: "$message.header#/MQMD/CorrelId" 61 | contentType: application/json 62 | payload: 63 | $ref: "#/components/schemas/lightMeasuredPayload" 64 | dimLight: 65 | name: dimLight 66 | title: Dim light 67 | summary: Command a particular streetlight to dim the lights. 68 | correlationId: 69 | $ref: "#/components/correlationIds/sentAtCorrelator" 70 | payload: 71 | $ref: "#/components/schemas/dimLightPayload" 72 | 73 | schemas: 74 | lightMeasuredPayload: 75 | type: object 76 | properties: 77 | lumens: 78 | type: integer 79 | minimum: 0 80 | description: Light intensity measured in lumens. 81 | sentAt: 82 | $ref: "#/components/schemas/sentAt" 83 | dimLightPayload: 84 | type: object 85 | properties: 86 | percentage: 87 | type: integer 88 | description: Percentage to which the light should be dimmed to. 89 | minimum: 0 90 | maximum: 100 91 | sentAt: 92 | $ref: "#/components/schemas/sentAt" 93 | sentAt: 94 | type: string 95 | format: date-time 96 | description: Date and time when the message was sent. 97 | 98 | parameters: 99 | streetlightId: 100 | description: The ID of the streetlight. 101 | schema: 102 | type: string 103 | 104 | correlationIds: 105 | sentAtCorrelator: 106 | description: Data from message payload used as correlation ID 107 | location: $message.payload#/sentAt 108 | `; 109 | -------------------------------------------------------------------------------- /playground/specs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './anyOf'; 2 | export * from './application-headers'; 3 | export * from './circular'; 4 | export * from './correlation-id'; 5 | export * from './dummy'; 6 | export * from './gitter-streaming'; 7 | export * from './invalid'; 8 | export * from './not'; 9 | export * from './oneOf'; 10 | export * from './rpc-client'; 11 | export * from './rpc-server'; 12 | export * from './slack-rtm'; 13 | export * from './streetlights'; 14 | -------------------------------------------------------------------------------- /playground/specs/invalid.ts: -------------------------------------------------------------------------------- 1 | export const invalid = ` 2 | components: 3 | messages: 4 | RideUpdated: 5 | payload: 6 | type: object 7 | schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0 8 | contentType: application/json 9 | PaymentCharged: 10 | payload: 11 | type: object 12 | schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0 13 | contentType: application/json 14 | channels: 15 | 'taxinyc/backoffice/payment/charged/v1/{payment_status}/{driver_id}/{passenger_id}': 16 | publish: 17 | x-scs-function-name: processPayment 18 | message: 19 | $ref: '#/components/messages/PaymentCharged' 20 | parameters: 21 | driver_id: 22 | schema: 23 | type: string 24 | payment_status: 25 | schema: 26 | type: string 27 | 'taxinyc/ops/ride/updated/v1/{ride_status}/{driver_id}/{passenger_id}/{current_latitude}/{current_longitude}/{long_property}': 28 | subscribe: 29 | x-scs-function-name: processPayment 30 | x-scs-destination: test/taxinyc/PaymentProcessorQueue 31 | message: 32 | $ref: '#/components/messages/RideUpdated' 33 | asyncapi: 2.0.0 34 | info: 35 | title: ProcessPayment 36 | version: 0.0.1 37 | `; 38 | -------------------------------------------------------------------------------- /playground/specs/not.ts: -------------------------------------------------------------------------------- 1 | export const not = ` 2 | asyncapi: '2.0.0' 3 | info: 4 | title: Not example 5 | version: '1.0.0' 6 | 7 | channels: 8 | test: 9 | publish: 10 | message: 11 | $ref: '#/components/messages/testMessages' 12 | 13 | components: 14 | messages: 15 | testMessages: 16 | payload: 17 | $ref: "#/components/schemas/testSchema" 18 | 19 | schemas: 20 | testSchema: 21 | type: object 22 | properties: 23 | key: 24 | not: 25 | type: integer 26 | `; 27 | -------------------------------------------------------------------------------- /playground/specs/oneOf.ts: -------------------------------------------------------------------------------- 1 | export const oneOf = ` 2 | asyncapi: '2.0.0' 3 | info: 4 | title: OneOf example 5 | version: '1.0.0' 6 | 7 | channels: 8 | test: 9 | publish: 10 | message: 11 | $ref: '#/components/messages/testMessages' 12 | 13 | test2: 14 | subscribe: 15 | message: 16 | # Use oneOf here if different messages are published on test2 topic. 17 | oneOf: 18 | - payload: 19 | $ref: "#/components/schemas/objectWithKey" 20 | - payload: 21 | $ref: "#/components/schemas/objectWithKey2" 22 | 23 | components: 24 | messages: 25 | testMessages: 26 | payload: 27 | oneOf: # oneOf in payload schema 28 | - $ref: "#/components/schemas/objectWithKey" 29 | - $ref: "#/components/schemas/objectWithKey2" 30 | testMessage1: 31 | payload: 32 | $ref: "#/components/schemas/objectWithKey" 33 | testMessage2: 34 | payload: 35 | $ref: "#/components/schemas/objectWithKey2" 36 | 37 | schemas: 38 | objectWithKey: 39 | type: object 40 | properties: 41 | key: 42 | type: string 43 | objectWithKey2: 44 | type: object 45 | properties: 46 | key2: 47 | type: string 48 | `; 49 | -------------------------------------------------------------------------------- /playground/specs/rpc-client.ts: -------------------------------------------------------------------------------- 1 | export const rpcClient = ` 2 | asyncapi: '2.0.0' 3 | id: 'urn:rpc:example:client' 4 | defaultContentType: application/json 5 | 6 | info: 7 | title: RPC Client Example 8 | description: This example demonstrates how to define an RPC client. 9 | version: '1.0.0' 10 | 11 | servers: 12 | production: 13 | url: rabbitmq.example.org 14 | protocol: amqp 15 | 16 | channels: 17 | '{queue}': 18 | parameters: 19 | queue: 20 | schema: 21 | type: string 22 | pattern: '^amq\\.gen\\-.+$' 23 | bindings: 24 | amqp: 25 | is: queue 26 | queue: 27 | exclusive: true 28 | subscribe: 29 | operationId: receiveSumResult 30 | bindings: 31 | amqp: 32 | ack: false 33 | message: 34 | correlationId: 35 | location: $message.header#/correlation_id 36 | payload: 37 | type: object 38 | properties: 39 | result: 40 | type: number 41 | examples: 42 | - 7 43 | 44 | rpc_queue: 45 | bindings: 46 | amqp: 47 | is: queue 48 | queue: 49 | durable: false 50 | publish: 51 | operationId: requestSum 52 | bindings: 53 | amqp: 54 | ack: true 55 | message: 56 | bindings: 57 | amqp: 58 | replyTo: 59 | type: string 60 | correlationId: 61 | location: $message.header#/correlation_id 62 | payload: 63 | type: object 64 | properties: 65 | numbers: 66 | type: array 67 | items: 68 | type: number 69 | examples: 70 | - [4,3] 71 | `; 72 | -------------------------------------------------------------------------------- /playground/specs/rpc-server.ts: -------------------------------------------------------------------------------- 1 | export const rpcServer = ` 2 | asyncapi: '2.0.0' 3 | id: 'urn:rpc:example:server' 4 | defaultContentType: application/json 5 | 6 | info: 7 | title: RPC Server Example 8 | description: This example demonstrates how to define an RPC server. 9 | version: '1.0.0' 10 | 11 | servers: 12 | production: 13 | url: rabbitmq.example.org 14 | protocol: amqp 15 | 16 | channels: 17 | '{queue}': 18 | parameters: 19 | queue: 20 | schema: 21 | type: string 22 | pattern: '^amq\\.gen\\-.+$' 23 | bindings: 24 | amqp: 25 | is: queue 26 | queue: 27 | exclusive: true 28 | publish: 29 | operationId: sendSumResult 30 | bindings: 31 | amqp: 32 | ack: true 33 | message: 34 | correlationId: 35 | location: $message.header#/correlation_id 36 | payload: 37 | type: object 38 | properties: 39 | result: 40 | type: number 41 | examples: 42 | - 7 43 | 44 | rpc_queue: 45 | bindings: 46 | amqp: 47 | is: queue 48 | queue: 49 | durable: false 50 | subscribe: 51 | operationId: sum 52 | message: 53 | bindings: 54 | amqp: 55 | replyTo: 56 | type: string 57 | correlationId: 58 | location: $message.header#/correlation_id 59 | payload: 60 | type: object 61 | properties: 62 | numbers: 63 | type: array 64 | items: 65 | type: number 66 | examples: 67 | - [4,3] 68 | `; 69 | -------------------------------------------------------------------------------- /playground/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | }; 14 | export default config; 15 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /playground/utils/defaultConfig.ts: -------------------------------------------------------------------------------- 1 | export const defaultConfig = `{ 2 | "show": { 3 | "sidebar": false, 4 | "info": true, 5 | "operations": true, 6 | "servers": true, 7 | "messages": true, 8 | "schemas": true, 9 | "errors": true 10 | }, 11 | "expand":{ 12 | "messageExamples": false 13 | }, 14 | "sidebar": { 15 | "showServers": "byDefault", 16 | "showOperations": "byDefault" 17 | } 18 | }`; 19 | -------------------------------------------------------------------------------- /playground/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-types 2 | export const parse = (str?: string): T => { 3 | if (!str) { 4 | return {} as T; 5 | } 6 | 7 | try { 8 | return JSON.parse(str) as T; 9 | } catch (e) { 10 | return {} as T; 11 | } 12 | }; 13 | 14 | // eslint-disable-next-line @typescript-eslint/ban-types 15 | export const stringify = (content?: T): string => { 16 | if (!content) { 17 | return ''; 18 | } 19 | 20 | try { 21 | return JSON.stringify(content); 22 | } catch (e) { 23 | return ''; 24 | } 25 | }; 26 | 27 | export const fetchSchema = async (link: string): Promise => { 28 | const requestOptions = { 29 | method: 'GET', 30 | }; 31 | 32 | return fetch(link, requestOptions).then(handleResponse); 33 | }; 34 | 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | function handleResponse(response: any) { 37 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 38 | return response.text().then((data: string) => data); 39 | } 40 | 41 | export function debounce( 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | func: (...args: any[]) => void, 44 | wait: number, 45 | onStart: () => void, 46 | onCancel: () => void, 47 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 48 | ): () => any { 49 | let timeout: NodeJS.Timeout | undefined; 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | return (...args: any[]) => { 52 | if (timeout) { 53 | clearTimeout(timeout); 54 | } 55 | onStart(); 56 | timeout = setTimeout(() => { 57 | timeout = undefined; 58 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 59 | func(...args); 60 | onCancel(); 61 | }, wait || 1000); 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /playground/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './defaultConfig'; 3 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "es5", 5 | "lib": ["es5", "es6", "dom", "es2017", "es2019"], 6 | "sourceMap": true, 7 | "inlineSources": true, 8 | "allowJs": false, 9 | "jsx": "react", 10 | "removeComments": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "alwaysStrict": true, 16 | "strictFunctionTypes": false, 17 | "forceConsistentCasingInFileNames": true, 18 | "moduleResolution": "node", 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noImplicitAny": true, 22 | "strictNullChecks": true, 23 | "noUnusedLocals": true, 24 | "resolveJsonModule": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web-component/bump-react-comp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #Use in CI only 4 | #Purpos of this script is to use it to install/bump provided version of the React component 5 | 6 | # sleep for 120 seconds before using latest version in web-component, because sometimes NPM needs additional few seconds to `save` package in registry 7 | sleep 2m 8 | echo "Log all versions of the package on npm registry" 9 | npm show @asyncapi/react-component versions 10 | echo "Log latest version of the package on npm registry" 11 | npm show @asyncapi/react-component dist-tags.latest 12 | echo "We need to uninstall old version first to make sure package.json will get updated latest version number" 13 | npm uninstall @asyncapi/react-component 14 | echo "Starting installation of @asyncapi/react-component@$VERSION in $PWD" 15 | npm install @asyncapi/react-component@$VERSION --save --loglevel verbose -------------------------------------------------------------------------------- /web-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@asyncapi/web-component", 3 | "version": "2.6.3", 4 | "private": false, 5 | "description": "A web component for AsyncAPI specification. Based on @asyncapi/react-component.", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/asyncapi/asyncapi-react" 9 | }, 10 | "author": { 11 | "name": "The AsyncAPI maintainers", 12 | "url": "https://www.asyncapi.com" 13 | }, 14 | "license": "Apache-2.0", 15 | "bugs": { 16 | "url": "https://github.com/asyncapi/asyncapi-react/issues" 17 | }, 18 | "keywords": [ 19 | "asyncapi", 20 | "asyncapi-specification", 21 | "webcomponent", 22 | "web-component", 23 | "event" 24 | ], 25 | "tags": [ 26 | "asyncapi", 27 | "asyncapi-specification", 28 | "webcomponent", 29 | "web-component", 30 | "event" 31 | ], 32 | "main": "lib/index.js", 33 | "types": "lib/index.d.ts", 34 | "files": [ 35 | "/lib", 36 | "./README.md", 37 | "./LICENSE" 38 | ], 39 | "scripts": { 40 | "start": "tsc --watch", 41 | "bundle": "webpack", 42 | "prepack": "cp ../README.md ./README.md && cp ../LICENSE ./LICENSE", 43 | "postpack": "rm -rf ./README.md && rm -rf ./LICENSE", 44 | "install:reactcomp": "chmod +x ./bump-react-comp.sh && ./bump-react-comp.sh" 45 | }, 46 | "dependencies": { 47 | "@asyncapi/react-component": "^2.6.3", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "web-react-components": "^1.4.2" 51 | }, 52 | "devDependencies": { 53 | "@types/react": "^18", 54 | "node-polyfill-webpack-plugin": "^2.0.1", 55 | "ts-loader": "9.4.4", 56 | "webpack": "5.88.2", 57 | "webpack-cli": "5.1.4" 58 | }, 59 | "publishConfig": { 60 | "access": "public" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web-component/src/AsyncApiWebComponent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unescaped-entities */ 2 | import * as React from 'react'; 3 | // @ts-expect-error no types exists 4 | import { register } from 'web-react-components'; 5 | import AsyncApiComponent, { 6 | AsyncApiProps, 7 | FetchingSchemaInterface, 8 | } from '@asyncapi/react-component'; 9 | 10 | /** 11 | * Angular 11.0.7 running in 'production mode + enforced stricter type 12 | checking and stricter bundle budgets' mode showed requirement of explicit 13 | definition of conditions, hence slightly visually excessive code. 14 | Both undefined and 'undefined' are used because first has typeof 15 | 'undefined' and the second has typeof 'string'. And these differences do 16 | matter and work differently in current Angular's usecase, because Angular 17 | calls 'AsyncApiWebComponent.prototype.render' multiple times for each 18 | property specified in function 'register', assigning primitive type 19 | `undefined` to undefined properties first time and assigning string 20 | `'undefined'` each next time. 21 | */ 22 | function retrieveSchemaProp( 23 | props: AsyncApiWebComponentProps, 24 | ): FetchingSchemaInterface { 25 | let schemaUrl = props.schemaUrl; 26 | let schemaFetchOptions = props.schemaFetchOptions; 27 | 28 | if (!schemaUrl || schemaUrl === 'undefined') { 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any 30 | schemaUrl = undefined as any; 31 | } 32 | if (!schemaFetchOptions) { 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any 34 | schemaFetchOptions = undefined as any; 35 | } 36 | 37 | let schema = props.schema || {}; 38 | if (schemaUrl) { 39 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 40 | const schemaRequestOptions = schemaFetchOptions 41 | ? JSON.parse(JSON.stringify(schemaFetchOptions)) 42 | : (schema as FetchingSchemaInterface).requestOptions ?? {}; 43 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 44 | schema = { url: schemaUrl, requestOptions: schemaRequestOptions }; 45 | } 46 | 47 | return schema as FetchingSchemaInterface; // NOSONAR 48 | } 49 | 50 | export interface AsyncApiWebComponentProps extends AsyncApiProps { 51 | cssImportPath?: string; 52 | schemaFetchOptions?: RequestInit; 53 | schemaUrl: string; 54 | } 55 | 56 | export class AsyncApiWebComponent extends React.Component { 57 | private lastUrlCheck: number = Date.now(); 58 | 59 | shouldComponentUpdate(nextProps: Readonly) { 60 | const prevSchema = retrieveSchemaProp(this.props); 61 | const nextSchema = retrieveSchemaProp(nextProps); 62 | 63 | if (!prevSchema || !nextSchema || !prevSchema.url || !nextSchema.url) { 64 | return true; 65 | } 66 | if (prevSchema.url === nextSchema.url) { 67 | const dateNow = Date.now(); 68 | // if the difference between updates of the same urls occurred less or equal than 25 milliseconds, consider no change. 69 | if (this.lastUrlCheck <= dateNow - 25) { 70 | this.lastUrlCheck = dateNow; 71 | return true; 72 | } 73 | return false; 74 | } 75 | return true; 76 | } 77 | 78 | render() { 79 | const props = this.props; 80 | const finalCssImportPath = props.cssImportPath ?? 'assets/default.min.css'; 81 | const schema = retrieveSchemaProp(this.props); 82 | 83 | return ( 84 | <> 85 | 86 | 87 | 88 | ); 89 | } 90 | } 91 | 92 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 93 | register(AsyncApiWebComponent, 'asyncapi-component', [ 94 | 'schema', 95 | 'schemaFetchOptions', 96 | 'schemaUrl', 97 | 'config', 98 | 'cssImportPath', 99 | ]); 100 | -------------------------------------------------------------------------------- /web-component/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AsyncApiWebComponent } from './AsyncApiWebComponent'; 2 | export default AsyncApiWebComponent; 3 | -------------------------------------------------------------------------------- /web-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "baseUrl": "./src", 6 | "declaration": true, 7 | "ignoreDeprecations": "5.0" 8 | }, 9 | "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"], 10 | "exclude": ["lib/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /web-component/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | 'asyncapi-web-component': './src/index.ts', 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'lib'), 10 | filename: '[name].js', 11 | libraryTarget: 'umd', 12 | library: 'AsyncApiWebComponent', 13 | libraryExport: 'default', 14 | umdNamedDefine: true, 15 | }, 16 | 17 | mode: 'production', 18 | target: 'web', 19 | resolve: { 20 | extensions: ['.ts', '.tsx', '.js'], 21 | alias: { 22 | // this is important as it makes sure react is resolved only once from both library and web-component -> otherwise react may be bundled twice and an error as follows occur 23 | // "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 24 | // 1. You might have mismatching versions of React and the renderer (such as React DOM) 25 | // 2. You might be breaking the Rules of Hooks 26 | // 3. You might have more than one copy of React in the same app 27 | // See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." 28 | react: path.resolve('../node_modules/react'), 29 | }, 30 | fallback: { 31 | fs: false, 32 | }, 33 | }, 34 | devtool: 'source-map', 35 | 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.tsx?$/, 40 | loader: 'ts-loader', 41 | exclude: /node_modules/, 42 | }, 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | /** 48 | * Uncomment plugin when you wanna see dependency map of bundled package 49 | */ 50 | // new BundleAnalyzerPlugin(), 51 | new NodePolyfillPlugin(), 52 | ], 53 | }; 54 | --------------------------------------------------------------------------------