├── .eslintignore ├── playground ├── .env.example ├── app.vue ├── public │ └── favicon.ico ├── nuxt.config.ts ├── pages │ ├── github-link.vue │ ├── file-contributors.vue │ ├── readme.vue │ ├── index.vue │ ├── releases.vue │ ├── last-release.vue │ ├── release.vue │ ├── contributors.vue │ └── commits.vue └── components │ └── Navigation.vue ├── .npmrc ├── docs ├── tsconfig.json ├── public │ ├── icon.png │ └── favicon.ico ├── renovate.json ├── .gitignore ├── .env.example ├── components │ └── content │ │ ├── examples │ │ ├── ReleaseExample.vue │ │ ├── RepositoryExample.vue │ │ ├── ReadmeExample.vue │ │ ├── ContributorsExample.vue │ │ ├── LastReleaseExample.vue │ │ ├── ReleasesExample.vue │ │ ├── Contributor.vue │ │ ├── FileContributorsExample.vue │ │ └── Repository.vue │ │ └── Ellipsis.vue ├── README.md ├── nuxt.config.ts ├── content │ ├── 1.index.md │ ├── 4.composables.md │ ├── 2.configuration.md │ └── 3.components.md └── theme.config.ts ├── tsconfig.json ├── test ├── fixtures │ └── basic │ │ ├── package.json │ │ ├── nuxt.config.ts │ │ └── app.vue └── module.test.ts ├── .github ├── FUNDING.yml ├── scripts │ ├── example.sh │ ├── clean.sh │ ├── test.sh │ ├── release-edge.sh │ └── bump-edge.ts ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ └── bug-report.yml ├── workflows │ └── ci.yml └── PULL_REQUEST_TEMPLATE.md ├── src ├── runtime │ ├── types │ │ ├── commits.d.ts │ │ ├── index.d.ts │ │ ├── contributors.ts │ │ ├── releases.d.ts │ │ └── repository.d.ts │ ├── components │ │ ├── GithubReadme.ts │ │ ├── GithubCommits.ts │ │ ├── GithubRepository.ts │ │ ├── GithubLastRelease.ts │ │ ├── GithubContributors.ts │ │ ├── GithubRelease.ts │ │ ├── GithubFileContributors.ts │ │ ├── GithubReleases.ts │ │ └── GithubLink.ts │ ├── server │ │ ├── api │ │ │ ├── repository.ts │ │ │ ├── contributors │ │ │ │ ├── index.ts │ │ │ │ └── file.ts │ │ │ ├── commits │ │ │ │ └── index.ts │ │ │ ├── releases │ │ │ │ └── index.ts │ │ │ └── readme.ts │ │ └── utils │ │ │ └── queries.ts │ └── composables │ │ └── useGithub.ts └── module.ts ├── .editorconfig ├── .eslintrc ├── SECURITY.md ├── .gitignore ├── package.json ├── README.md └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /playground/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN= 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxtlabs/github-module/HEAD/docs/public/icon.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxtlabs/github-module/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "github-module-test-basic" 4 | } 5 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxtlabs/github-module/HEAD/playground/public/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nuxt] 4 | open_collective: nuxtjs 5 | -------------------------------------------------------------------------------- /docs/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ], 5 | "lockFileMaintenance": { 6 | "enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | sw.* 11 | .env 12 | .output 13 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | # Create one with no scope selected on https://github.com/settings/tokens/new 2 | # This token is used for fetching the repository releases. 3 | GITHUB_TOKEN= -------------------------------------------------------------------------------- /docs/components/content/examples/ReleaseExample.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/types/commits.d.ts: -------------------------------------------------------------------------------- 1 | import { GithubRepositoryOptions } from '.' 2 | 3 | export interface GithubCommitsQuery extends GithubRepositoryOptions { 4 | date?: string 5 | source?: string 6 | } 7 | -------------------------------------------------------------------------------- /docs/components/content/examples/RepositoryExample.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.github/scripts/example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EXAMPLE_PATH=examples/$1 4 | 5 | if [[ ! -d "$EXAMPLE_PATH/node_modules" ]] ; then 6 | (cd $EXAMPLE_PATH && yarn install) 7 | fi 8 | 9 | (cd $EXAMPLE_PATH && yarn dev) 10 | -------------------------------------------------------------------------------- /docs/components/content/examples/ReadmeExample.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /test/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import githubModule from '../../../src/module' 3 | 4 | export default defineNuxtConfig({ 5 | modules: ['@nuxt/content', githubModule], 6 | github: { repo: 'nuxt/nuxt.js' } 7 | }) 8 | -------------------------------------------------------------------------------- /src/runtime/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './repository' 2 | export * from './releases' 3 | export * from './contributors' 4 | export * from './commits' 5 | 6 | 7 | export interface GithubAuthor { 8 | name: string 9 | login: string 10 | avatarUrl: string 11 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nuxtjs/eslint-config-typescript"], 4 | "rules": { 5 | "vue/multi-word-component-names": "off", 6 | "vue/no-multiple-template-root": "off", 7 | "no-redeclare": "off", 8 | "import/named": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import githubModule from '../src/module' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [githubModule, '@nuxt/content'], 5 | github: { 6 | repo: 'nuxt/content' 7 | }, 8 | experimental: { 9 | payloadExtraction: false 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | If you discover a security vulnerability in github-module please disclose it via [our huntr page](https://huntr.dev/repos/nuxtlabs/github-module/). Information about bounties, CVEs, response times and past reports are all there.. 4 | 5 | Thank you for improving the security of github-module. -------------------------------------------------------------------------------- /src/runtime/types/contributors.ts: -------------------------------------------------------------------------------- 1 | import { GithubRepositoryOptions } from '.' 2 | 3 | export interface GithubContributorsQuery extends GithubRepositoryOptions { 4 | source?: string 5 | max?: string | number 6 | } 7 | 8 | export interface GithubRawContributor { 9 | avatar_url: string 10 | login: string 11 | name: string 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: 📚 Documentation 3 | url: https://nuxt-github.netlify.app 4 | about: Check documentation for usage 5 | - name: 💬 Discussions 6 | url: https://github.com/nuxtlabs/github-module/discussions 7 | about: Use discussions if you have an idea for improvement and asking questions 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | [![nuxt-content](/docs/public/cover_dark.png "@nuxt/content image")](https://content.nuxtjs.org) 2 | 3 | # Documentation 4 | 5 | This documentation uses [Docus](https://github.com/nuxtlabs/docus). 6 | 7 | ## 💻 Development 8 | 9 | - Install dependencies using `yarn install` 10 | - Start using `yarn dev` 11 | - Build using `yarn build` 12 | -------------------------------------------------------------------------------- /docs/components/content/examples/ContributorsExample.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/components/content/examples/LastReleaseExample.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | app: { 3 | }, 4 | extends: [ 5 | '../node_modules/@nuxt-themes/docus' 6 | ], 7 | modules: [ 8 | '../src/module.ts' 9 | ], 10 | github: { 11 | repo: 'nuxt/content' 12 | }, 13 | components: [ 14 | { 15 | path: '~/components', 16 | prefix: '', 17 | global: true 18 | } 19 | ] 20 | }) 21 | -------------------------------------------------------------------------------- /.github/scripts/clean.sh: -------------------------------------------------------------------------------- 1 | # Docs 2 | rm -rf docs/.nuxt 3 | rm -rf docs/.output 4 | rm -rf docs/dist 5 | 6 | # Playground 7 | rm -rf playground/.nuxt 8 | rm -rf playground/.output 9 | rm -rf playground/dist 10 | 11 | # Fixture 12 | rm -rf test/fixtures/basic/.nuxt 13 | rm -rf test/fixtures/basic/.output 14 | rm -rf test/fixtures/basic/dist 15 | 16 | # Base 17 | rm -rf yarn.lock 18 | rm -rf node_modules 19 | rm -rf dist 20 | -------------------------------------------------------------------------------- /docs/components/content/examples/ReleasesExample.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /docs/components/content/examples/Contributor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /.github/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CWD=$(pwd) 4 | ARG1=${1} 5 | 6 | # Remove all .nuxt directories in the test/fixtures directory 7 | for d in $(find $CWD/test/fixtures -maxdepth 1 -mindepth 1 -type d); do 8 | cd $d 9 | rm -rf .nuxt 10 | npx nuxi prepare 11 | cd $CWD 12 | done 13 | 14 | if [[ $ARG1 ]] 15 | then 16 | echo "npx vitest run -t $ARG1" 17 | (npx vitest run -t $ARG1.test) 18 | else 19 | echo "npx vitest run" 20 | (npx vitest run) 21 | fi 22 | -------------------------------------------------------------------------------- /test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /docs/components/content/Ellipsis.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /docs/components/content/examples/FileContributorsExample.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: '16' 20 | - uses: pnpm/action-setup@v2.2.4 21 | name: Install pnpm 22 | id: pnpm-install 23 | with: 24 | version: 7 25 | - run: pnpm install 26 | - run: pnpm prepare 27 | - run: pnpm lint 28 | - run: pnpm build 29 | - run: pnpm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | .history 39 | 40 | # Intellij idea 41 | *.iml 42 | .idea 43 | 44 | # OSX 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | -------------------------------------------------------------------------------- /playground/pages/github-link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /src/runtime/types/releases.d.ts: -------------------------------------------------------------------------------- 1 | import { GithubRepositoryOptions } from '.' 2 | 3 | export interface GithubReleaseQuery extends GithubRepositoryOptions { 4 | tag?: string 5 | } 6 | 7 | export interface GithubReleasesQuery extends GithubReleaseQuery { 8 | per_page?: string 9 | page?: string 10 | last?: boolean 11 | } 12 | 13 | export interface GithubRawRelease { 14 | name: string 15 | body: string 16 | v: number 17 | tag_name: string 18 | date: number 19 | url: string 20 | tarball: string 21 | zipball: string 22 | prerelease: boolean 23 | reactions: Array 24 | author: { 25 | name: string 26 | url: string 27 | avatar: string 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /playground/pages/file-contributors.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /.github/scripts/release-edge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Temporary forked from nuxt/framework 4 | 5 | set -xe 6 | 7 | # Restore all git changes 8 | git restore -s@ -SW -- . 9 | 10 | # Bump versions to edge 11 | yarn jiti ./.github/scripts/bump-edge 12 | 13 | # Build 14 | yarn build 15 | 16 | # Update token 17 | if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then 18 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc 19 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 20 | echo "always-auth=true" >> ~/.npmrc 21 | echo "npmAuthToken: ${NODE_AUTH_TOKEN}" >> ~/.yarnrc.yml 22 | npm whoami 23 | fi 24 | 25 | # Release packages 26 | echo "Publishing package..." 27 | npm publish --access public --tolerate-republish 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature Request" 3 | about: Suggest an idea or enhancement for the module. 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ### Is your feature request related to a problem? Please describe 10 | 11 | 12 | ### Describe the solution you'd like 13 | 14 | 15 | ### Describe alternatives you've considered 16 | 17 | 18 | ### Additional context 19 | 20 | -------------------------------------------------------------------------------- /playground/pages/readme.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 25 | -------------------------------------------------------------------------------- /test/module.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | import { describe, expect, test } from 'vitest' 3 | import { $fetch, setup } from '@nuxt/test-utils' 4 | 5 | describe('fixtures:basic', async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), 8 | server: true 9 | }) 10 | 11 | test('List releases', async () => { 12 | const releases = await $fetch('/api/_github/releases/index.json') 13 | 14 | expect(releases.length).greaterThan(0) 15 | 16 | releases.forEach((component) => { 17 | expect(component).ownProperty('name') 18 | expect(component).ownProperty('date') 19 | expect(component).ownProperty('body') 20 | expect(component).ownProperty('v') 21 | expect(component).ownProperty('url') 22 | expect(component).ownProperty('author') 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /docs/content/1.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@nuxtlabs/github" 3 | description: "@nuxtlabs/github is a Nuxt module for first class integration with GitHub." 4 | navigation: false 5 | layout: fluid 6 | --- 7 | 8 | ::block-hero 9 | --- 10 | cta: 11 | - Get Started 12 | - /configuration 13 | secondary: 14 | - Star on GitHub → 15 | - https://github.com/nuxtlabs/github-module 16 | snippet: yarn add @nuxtlabs/github-module 17 | --- 18 | 19 | #title 20 | GitHub Module 21 | 22 | #description 23 | Nuxt module for first class integration with [GitHub](https://github.com). 24 | 25 | #extra 26 | ::list 27 | - Repository informations 28 | - Releases 29 | - Repository contributors 30 | - File contributors fetching 31 | - Readme file fetching 32 | - GithubLink component 33 | - [@remark/gfm](https://github.com/remarkjs/remark-gfm) plugin for @nuxt/content 34 | - Parse releases notes with @nuxt/content 35 | :: 36 | :: 37 | -------------------------------------------------------------------------------- /src/runtime/components/GithubReadme.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots, PropType } from 'vue' 2 | import { hash } from 'ohash' 3 | import { useGithub } from '../composables/useGithub' 4 | import { GithubRepositoryOptions } from '../types' 5 | // @ts-ignore 6 | import { useAsyncData } from '#imports' 7 | 8 | export default defineComponent({ 9 | props: { 10 | query: { 11 | type: Object as PropType, 12 | required: false, 13 | default: () => ({}) 14 | } 15 | }, 16 | async setup (props) { 17 | const { fetchReadme } = useGithub() 18 | 19 | const { data: readme, refresh, pending } = await useAsyncData(`github-readme-${hash(props.query)}`, () => fetchReadme(props.query)) 20 | 21 | return { 22 | readme, 23 | refresh, 24 | pending 25 | } 26 | }, 27 | render ({ readme, refresh, pending }) { 28 | const slots = useSlots() 29 | 30 | return slots?.default?.({ readme, refresh, pending }) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /src/runtime/components/GithubCommits.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots } from 'vue' 2 | import type { PropType } from 'vue' 3 | import { hash } from 'ohash' 4 | import { useGithub } from '../composables/useGithub' 5 | import { GithubCommitsQuery } from '../types' 6 | // @ts-ignore 7 | import { useAsyncData } from '#imports' 8 | 9 | export default defineComponent({ 10 | props: { 11 | query: { 12 | type: Object as PropType, 13 | required: false, 14 | default: () => ({}) 15 | } 16 | }, 17 | async setup (props) { 18 | const { fetchCommits } = useGithub() 19 | 20 | const { data: commits, pending, refresh } = await useAsyncData(`github-commits-${hash(props.query)}`, () => fetchCommits(props.query)) 21 | return { 22 | commits, 23 | pending, 24 | refresh 25 | } 26 | }, 27 | render ({ commits, pending, refresh }) { 28 | const slots = useSlots() 29 | return slots?.default?.({ commits, pending, refresh }) 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/runtime/components/GithubRepository.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots, PropType } from 'vue' 2 | import { hash } from 'ohash' 3 | import { useGithub } from '../composables/useGithub' 4 | import { GithubRepositoryOptions } from '../types' 5 | // @ts-ignore 6 | import { useAsyncData } from '#imports' 7 | 8 | export default defineComponent({ 9 | props: { 10 | query: { 11 | type: Object as PropType, 12 | required: false, 13 | default: () => ({}) 14 | } 15 | }, 16 | async setup (props) { 17 | const { fetchRepository } = useGithub() 18 | 19 | const { data: repository, refresh, pending } = await useAsyncData(`github-repository-${hash(props.query)}`, () => fetchRepository(props.query)) 20 | 21 | return { 22 | repository, 23 | refresh, 24 | pending 25 | } 26 | }, 27 | render ({ repository, refresh, pending }) { 28 | const slots = useSlots() 29 | 30 | return slots?.default?.({ repository, refresh, pending }) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /src/runtime/server/api/repository.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { overrideConfig, decodeParams, fetchRepository } from '../utils/queries' 4 | import type { GithubRepository, GithubRepositoryOptions } from '../../types' 5 | // @ts-ignore 6 | import { useRuntimeConfig, cachedEventHandler } from '#imports' 7 | 8 | const moduleConfig = useRuntimeConfig().github || {} 9 | 10 | const handler: typeof cachedEventHandler = process.env.NODE_ENV === 'development' || moduleConfig.disableCache ? eventHandler : cachedEventHandler 11 | 12 | export default handler(async (event: H3Event) => { 13 | // Get query 14 | const query = decodeParams(event.context.params?.query) as GithubRepositoryOptions 15 | 16 | // Merge query in module config 17 | const githubConfig = overrideConfig(moduleConfig, query) 18 | 19 | // Fetch repository from GitHub 20 | return await fetchRepository(githubConfig) as GithubRepository 21 | }, 22 | { 23 | maxAge: 60 // cache for one minute 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | -------------------------------------------------------------------------------- /src/runtime/components/GithubLastRelease.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots, PropType } from 'vue' 2 | import { hash } from 'ohash' 3 | import { useGithub } from '../composables/useGithub' 4 | import { GithubRepositoryOptions } from '../types' 5 | // @ts-ignore 6 | import { useAsyncData } from '#imports' 7 | 8 | export default defineComponent({ 9 | props: { 10 | query: { 11 | type: Object as PropType, 12 | required: false, 13 | default: () => ({}) 14 | } 15 | }, 16 | async setup (props) { 17 | const { fetchLastRelease } = useGithub() 18 | 19 | const { 20 | data: release, 21 | refresh, 22 | pending 23 | } = await useAsyncData(`github-last-release-${hash(props.query)}`, () => fetchLastRelease(props.query)) 24 | 25 | return { 26 | release, 27 | refresh, 28 | pending 29 | } 30 | }, 31 | render ({ release, refresh, pending }) { 32 | const slots = useSlots() 33 | 34 | return slots?.default?.({ release, refresh, pending }) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/runtime/server/api/contributors/index.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { overrideConfig, decodeParams, fetchRepositoryContributors } from '../../utils/queries' 4 | import type { GithubContributorsQuery } from '../../../types' 5 | // @ts-ignore 6 | import { useRuntimeConfig, cachedEventHandler } from '#imports' 7 | 8 | const moduleConfig = useRuntimeConfig().github || {} 9 | 10 | const handler: typeof cachedEventHandler = process.env.NODE_ENV === 'development' || moduleConfig.disableCache ? eventHandler : cachedEventHandler 11 | 12 | export default handler( 13 | async (event: H3Event) => { 14 | // Get query 15 | const query = decodeParams(event.context.params?.query) as GithubContributorsQuery 16 | 17 | // Merge query in module config 18 | const githubConfig = overrideConfig(moduleConfig, query) 19 | 20 | // Fetch contributors from GitHub 21 | return await fetchRepositoryContributors(query, githubConfig) 22 | }, 23 | { 24 | maxAge: 60 // cache for one minute 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /src/runtime/components/GithubContributors.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots } from 'vue' 2 | import type { PropType } from 'vue' 3 | import { hash } from 'ohash' 4 | import { useGithub } from '../composables/useGithub' 5 | import { GithubRepositoryOptions } from '../types' 6 | // @ts-ignore 7 | import { useAsyncData } from '#imports' 8 | 9 | export default defineComponent({ 10 | props: { 11 | query: { 12 | type: Object as PropType, 13 | required: false, 14 | default: () => ({}) 15 | } 16 | }, 17 | async setup (props) { 18 | const { fetchContributors } = useGithub() 19 | 20 | const { data: contributors, refresh, pending } = await useAsyncData(`github-contributors-${hash(props.query)}`, () => fetchContributors(props.query)) 21 | 22 | return { 23 | contributors, 24 | refresh, 25 | pending 26 | } 27 | }, 28 | render ({ contributors, refresh, pending }) { 29 | const slots = useSlots() 30 | 31 | return slots?.default?.({ contributors, refresh, pending }) 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /src/runtime/components/GithubRelease.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots, PropType } from 'vue' 2 | import { hash } from 'ohash' 3 | import { useGithub } from '../composables/useGithub' 4 | import { GithubReleaseQuery } from '../types' 5 | // @ts-ignore 6 | import { useAsyncData } from '#imports' 7 | 8 | export default defineComponent({ 9 | props: { 10 | query: { 11 | type: Object as PropType, 12 | required: false, 13 | default: () => ({}) 14 | } 15 | }, 16 | async setup (props) { 17 | const { fetchRelease } = useGithub() 18 | 19 | if (!props.query.tag) { return } 20 | 21 | const { 22 | data: release, 23 | refresh, 24 | pending 25 | } = await useAsyncData(`github-release-${hash(props.query)}`, () => fetchRelease(props.query)) 26 | 27 | return { 28 | release, 29 | refresh, 30 | pending 31 | } 32 | }, 33 | render ({ release, refresh, pending }) { 34 | const slots = useSlots() 35 | 36 | return slots?.default?.({ release, refresh, pending }) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /docs/content/4.composables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Composables" 3 | description: "Discover every composables from GitHub package." 4 | --- 5 | 6 | ## `useGithub()` 7 | 8 | `useGithub()`{lang="ts"} gives programmatic access to fetching functions used in `` components. 9 | 10 | ```ts 11 | const { 12 | // Fetch repository informations 13 | fetchRepository, 14 | // Fetch repository releases 15 | fetchReleases, 16 | // Fetch repository release by tag 17 | fetchRelease, 18 | // Fetch repository last release 19 | fetchLastRelease, 20 | // Fetch repository contributors 21 | fetchContributors, 22 | // Fetch a file contributors 23 | fetchFileContributors, 24 | // Fetch a readme file 25 | fetchReadme 26 | } = useGithub() 27 | ``` 28 | 29 | These helpers makes it easy to use the Github API with `useAsyncData`. 30 | 31 | ```ts 32 | const { data: repositoryInformations } = await useAsyncData( 33 | 'repository-informations', 34 | fetchRepository 35 | ) 36 | ``` 37 | 38 | ::source-link 39 | --- 40 | source: "/packages/github/src/runtime/composables/useGithub.ts" 41 | --- 42 | :: 43 | -------------------------------------------------------------------------------- /playground/pages/releases.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 36 | -------------------------------------------------------------------------------- /playground/pages/last-release.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 37 | -------------------------------------------------------------------------------- /src/runtime/server/api/commits/index.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { overrideConfig, decodeParams, fetchCommits } from '../../utils/queries' 4 | import type { GithubCommitsQuery } from '../../../types' 5 | // @ts-ignore 6 | import { useRuntimeConfig, cachedEventHandler } from '#imports' 7 | 8 | const moduleConfig = useRuntimeConfig().github || {} 9 | 10 | const handler: typeof cachedEventHandler = process.env.NODE_ENV === 'development' || moduleConfig.disableCache ? eventHandler : cachedEventHandler 11 | 12 | export default handler( 13 | async (event: H3Event) => { 14 | // Get query 15 | const query = decodeParams(event.context.params?.query) as GithubCommitsQuery 16 | const normalizedQuery = { 17 | ...query, 18 | date: query.date ? new Date(query.date) : undefined 19 | } 20 | 21 | // Merge query in module config 22 | const githubConfig = overrideConfig(moduleConfig, query) 23 | 24 | // Fetch contributors from GitHub 25 | return await fetchCommits(normalizedQuery, githubConfig) 26 | }, 27 | { 28 | maxAge: 60 // cache for one minute 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /src/runtime/server/api/contributors/file.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { overrideConfig, decodeParams, fetchFileContributors } from '../../utils/queries' 4 | import type { GithubContributorsQuery } from '../../../types' 5 | // @ts-ignore 6 | import { useRuntimeConfig, cachedEventHandler } from '#imports' 7 | 8 | const moduleConfig = useRuntimeConfig().github || {} 9 | 10 | const handler: typeof cachedEventHandler = process.env.NODE_ENV === 'development' || moduleConfig.disableCache ? eventHandler : cachedEventHandler 11 | 12 | export default handler( 13 | async (event: H3Event) => { 14 | // Get query 15 | const query = decodeParams(event.context.params?.query) as GithubContributorsQuery 16 | 17 | // Merge query in module config 18 | const githubConfig = overrideConfig(moduleConfig, query) 19 | 20 | // Use max from config if not send in query 21 | query.max = query.max ? Number(query.max) : moduleConfig.maxContributors 22 | 23 | // Fetch contributors from GitHub 24 | return await fetchFileContributors(query, githubConfig) 25 | }, 26 | { 27 | maxAge: 60 // cache for one minute 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /docs/theme.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from '@nuxt-themes/config' 2 | 3 | export default defineTheme( 4 | { 5 | title: 'GitHub Module', 6 | description: 'Integrate your GitHub repository informations in your Nuxt app.', 7 | layout: 'docs', 8 | url: 'https://nuxt-github-module.netlify.app', 9 | debug: false, 10 | socials: { 11 | twitter: '@nuxt_js', 12 | github: 'nuxtlabs/github-module' 13 | }, 14 | cover: { 15 | src: '/cover.png' 16 | }, 17 | github: { 18 | root: 'docs/content', 19 | edit: true, 20 | releases: true 21 | }, 22 | aside: { 23 | level: 0 24 | }, 25 | header: { 26 | logo: true 27 | }, 28 | footer: { 29 | credits: { 30 | icon: 'IconDocus', 31 | text: 'Powered by Docus', 32 | href: 'https://docus.com' 33 | }, 34 | icons: [ 35 | { 36 | label: 'NuxtJS', 37 | href: 'https://nuxtjs.org', 38 | component: 'IconNuxt' 39 | }, 40 | { 41 | label: 'Vue Telescope', 42 | href: 'https://vuetelescope.com', 43 | component: 'IconVueTelescope' 44 | } 45 | ] 46 | } 47 | } 48 | ) 49 | -------------------------------------------------------------------------------- /src/runtime/components/GithubFileContributors.ts: -------------------------------------------------------------------------------- 1 | import { hash } from 'ohash' 2 | import { defineComponent, toRef, useSlots, watch } from 'vue' 3 | import type { PropType } from 'vue' 4 | import { useGithub } from '../composables/useGithub' 5 | import { GithubContributorsQuery } from '../types' 6 | // @ts-ignore 7 | import { useAsyncData } from '#imports' 8 | 9 | export default defineComponent({ 10 | props: { 11 | query: { 12 | type: Object as PropType, 13 | required: false, 14 | default: () => ({}) 15 | } 16 | }, 17 | async setup (props) { 18 | const source = toRef(props.query, 'source') 19 | 20 | const { fetchFileContributors } = useGithub() 21 | watch(source, () => { 22 | if (refresh) { refresh() } 23 | }) 24 | 25 | const { data: contributors, refresh, pending } = await useAsyncData(`github-file-contributors-${hash(props.query)}`, () => fetchFileContributors(props.query)) 26 | 27 | return { 28 | contributors, 29 | refresh, 30 | pending 31 | } 32 | }, 33 | render ({ contributors, refresh, pending }) { 34 | const slots = useSlots() 35 | 36 | return slots?.default?.({ contributors, refresh, pending }) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /playground/pages/release.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### 🔗 Linked issue 6 | 7 | ### ❓ Type of change 8 | 9 | 10 | 11 | - [ ] 📖 Documentation (updates to the documentation or readme) 12 | - [ ] 🐞 Bug fix (a non-breaking change that fixes an issue) 13 | - [ ] 👌 Enhancement (improving an existing functionality like performance) 14 | - [ ] ✨ New feature (a non-breaking change that adds functionality) 15 | - [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change) 16 | 17 | ### 📚 Description 18 | 19 | 20 | 21 | 22 | 23 | ### 📝 Checklist 24 | 25 | 26 | 27 | 28 | 29 | - [ ] I have linked an issue or discussion. 30 | - [ ] I have updated the documentation accordingly. 31 | -------------------------------------------------------------------------------- /playground/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 58 | -------------------------------------------------------------------------------- /src/runtime/components/GithubReleases.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, useSlots } from 'vue' 2 | import type { PropType } from 'vue' 3 | import { hash } from 'ohash' 4 | import { useGithub } from '../composables/useGithub' 5 | import { GithubReleasesQuery } from '../types' 6 | 7 | // @ts-ignore 8 | import { useAsyncData } from '#imports' 9 | 10 | export default defineComponent({ 11 | props: { 12 | query: { 13 | type: Object as PropType, 14 | required: false, 15 | default: () => ({}) 16 | } 17 | }, 18 | async setup (props) { 19 | const { fetchReleases } = useGithub() 20 | 21 | // const id = `github-releases-${hash(props.query)}` 22 | 23 | const { data: releases, refresh, pending } = await useAsyncData(`github-releases-${hash(props.query)}`, () => fetchReleases(props.query)) 24 | 25 | // TODO: remove this painful workaround: hotfix for https://github.com/vuejs/core/issues/5513 26 | // @ts-ignore - Workaround 27 | // const releases = process.client ? useState(id) : ref() 28 | // releases.value = releases.value || _releases.value 29 | 30 | return { 31 | releases, 32 | refresh, 33 | pending 34 | } 35 | }, 36 | render ({ releases, refresh, pending }) { 37 | const slots = useSlots() 38 | 39 | return slots?.default?.({ releases, refresh, pending }) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /playground/pages/contributors.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 38 | -------------------------------------------------------------------------------- /docs/components/content/examples/Repository.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 55 | -------------------------------------------------------------------------------- /playground/pages/commits.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | -------------------------------------------------------------------------------- /src/runtime/server/api/releases/index.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { overrideConfig, decodeParams, fetchReleases, parseRelease } from '../../utils/queries' 4 | import type { GithubRawRelease, GithubReleasesQuery } from '../../../types' 5 | // @ts-ignore 6 | import { useRuntimeConfig, cachedEventHandler } from '#imports' 7 | 8 | const moduleConfig = useRuntimeConfig().github || {} 9 | 10 | const handler: typeof cachedEventHandler = process.env.NODE_ENV === 'development' || moduleConfig.disableCache ? eventHandler : cachedEventHandler 11 | 12 | export default handler( 13 | async (event: H3Event) => { 14 | // Get query 15 | const query = decodeParams(event.context.params?.query) as GithubReleasesQuery 16 | 17 | // Merge query in base config 18 | const githubConfig = overrideConfig(moduleConfig, query) 19 | 20 | if (!githubConfig.owner || !githubConfig.repo || !githubConfig.api) { return [] } 21 | // Fetches releases from GitHub 22 | let releases = (await fetchReleases(query, githubConfig)) as (GithubRawRelease | GithubRawRelease[]) 23 | 24 | if (!releases) { return } 25 | 26 | // Parse release notes when `parse` option is enabled and `@nuxt/content` is installed. 27 | if (moduleConfig.parseContents) { 28 | if (Array.isArray(releases)) { 29 | releases = await Promise.all(releases.map(release => parseRelease(release, githubConfig))) 30 | } else { 31 | return await parseRelease(releases, githubConfig) 32 | } 33 | } 34 | 35 | // Sort DESC by release version or date 36 | return (releases as GithubRawRelease[] || []).sort((a, b) => (a.v !== b.v ? b.v - a.v : a.date - b.date)).filter(Boolean) 37 | }, 38 | { 39 | maxAge: 60 // cache for one minute 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /src/runtime/server/api/readme.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { overrideConfig, fetchReadme, decodeParams } from '../utils/queries' 4 | import type { GithubRepositoryOptions, GithubRepositoryReadme } from '../../types' 5 | // @ts-ignore 6 | import { useRuntimeConfig, cachedEventHandler } from '#imports' 7 | 8 | const moduleConfig = useRuntimeConfig().github || {} 9 | 10 | const handler: typeof cachedEventHandler = process.env.NODE_ENV === 'development' || moduleConfig.disableCache ? eventHandler : cachedEventHandler 11 | 12 | export default handler(async (event: H3Event) => { 13 | // Get query 14 | const query = decodeParams(event.context.params?.query) as GithubRepositoryOptions 15 | 16 | // Merge query in base config 17 | const githubConfig = overrideConfig(moduleConfig, query) 18 | 19 | if (!githubConfig.owner || !githubConfig.repo || !githubConfig.api) { return [] } 20 | 21 | // Fetch readme from GitHub 22 | const readme = await fetchReadme(githubConfig) as GithubRepositoryReadme 23 | 24 | // Readme readable content 25 | const content = Buffer.from(readme.content ?? '', 'base64').toString() 26 | 27 | // Parse contents with @nuxt/content if enabled 28 | if (moduleConfig.parseContents) { 29 | // @ts-ignore 30 | const markdown = await import('@nuxt/content/transformers/markdown').then(m => m.default || m) 31 | // Return parsed content 32 | return await markdown.parse(`${githubConfig.owner}:${githubConfig.repo}:readme.md`, content, { 33 | markdown: { 34 | remarkPlugins: { 35 | // Use current Github repository for remark-github plugin 36 | 'remark-github': { 37 | repository: `${githubConfig.owner}/${githubConfig.repo}` 38 | } 39 | } 40 | } 41 | }) 42 | } 43 | 44 | return content 45 | }, 46 | { 47 | maxAge: 60 // cache for one minute 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /src/runtime/composables/useGithub.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedContent } from '@nuxt/content/dist/runtime/types' 2 | import { joinURL } from 'ufo' 3 | import type { GithubRepositoryOptions, GithubContributorsQuery, GithubReleasesQuery, GithubRepository, GithubRawRelease, GithubRawContributor, GithubReleaseQuery, GithubCommitsQuery } from '../types' 4 | import { useRequestEvent } from '#imports' 5 | 6 | export const useGithub = () => { 7 | const _fetch = (base: string) => (query: Q): Promise => { 8 | const url = joinURL('/api/_github', base, `${encodeParams(query) || 'index'}.json`) 9 | 10 | // add to nitro prerender 11 | if (process.server) { 12 | const event = useRequestEvent() 13 | event.node.res.setHeader( 14 | 'x-nitro-prerender', 15 | [event.node.res.getHeader('x-nitro-prerender'), url].filter(Boolean).join(',') 16 | ) 17 | } 18 | 19 | return $fetch(url, { responseType: 'json' }) 20 | } 21 | 22 | return { 23 | fetchRepository: _fetch('repository'), 24 | fetchReleases: _fetch('releases'), 25 | fetchRelease: _fetch('releases'), 26 | fetchLastRelease: (query: GithubRepositoryOptions): Promise => 27 | _fetch('releases')({ ...query, last: true } as any), 28 | fetchContributors: _fetch('contributors'), 29 | fetchFileContributors: _fetch('contributors/file'), 30 | fetchReadme: _fetch('readme'), 31 | fetchCommits: _fetch('commits') 32 | } 33 | } 34 | 35 | function encodeParams (params: any) { 36 | return Object.entries(params) 37 | .map(([key, value]) => `${key}_${String(value)}`) 38 | .join(':') 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxtlabs/github-module", 3 | "version": "1.6.3", 4 | "license": "MIT", 5 | "private": false, 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/module.mjs", 10 | "require": "./dist/module.cjs" 11 | } 12 | }, 13 | "main": "./dist/module.cjs", 14 | "types": "./dist/types.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "prepare": "nuxi prepare playground", 20 | "build": "nuxt-module-build", 21 | "dev": "nuxi dev playground", 22 | "dev:build": "nuxi build playground", 23 | "dev:docs": "nuxi dev docs", 24 | "build:docs": "nuxi generate docs", 25 | "lint": "eslint --ext .js,.ts,.vue .", 26 | "test": "vitest run", 27 | "prepack": "nuxt-module-build", 28 | "release": "release-it" 29 | }, 30 | "dependencies": { 31 | "@nuxt/kit": "^3.3.2", 32 | "@octokit/graphql": "^5.0.5", 33 | "@octokit/rest": "^19.0.7", 34 | "defu": "^6.1.2", 35 | "h3": "^1.6.2", 36 | "remark-gfm": "^3.0.1", 37 | "remark-github": "^11.2.4", 38 | "ufo": "^1.1.1" 39 | }, 40 | "devDependencies": { 41 | "@nuxt-themes/docus": "latest", 42 | "@nuxt/content": "latest", 43 | "@nuxt/module-builder": "latest", 44 | "@nuxt/test-utils": "^3.3.2", 45 | "@nuxtjs/eslint-config-typescript": "latest", 46 | "eslint": "latest", 47 | "nuxt": "^3.3.2", 48 | "release-it": "^15.9.3", 49 | "standard-version": "^9.5.0", 50 | "vitest": "^0.29.7" 51 | }, 52 | "packageManager": "pnpm@7.29.1", 53 | "pnpm": { 54 | "peerDependencyRules": { 55 | "allowedVersions": { 56 | "vue": "^3.2.45" 57 | }, 58 | "ignoreMissing": [ 59 | "webpack", 60 | "vue" 61 | ] 62 | } 63 | }, 64 | "release-it": { 65 | "hooks": { 66 | "before:init": [ 67 | "npm run build" 68 | ] 69 | }, 70 | "npm": { 71 | "access": "public" 72 | }, 73 | "git": { 74 | "commitMessage": "chore: release v${version}" 75 | }, 76 | "github": { 77 | "release": true, 78 | "releaseName": "v${version}" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug Report" 2 | description: Create a report to help us improve Nuxt 3 | labels: ["pending triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please carefully read the contribution docs before creating a bug report 9 | 👉 https://v3.nuxtjs.org/community/reporting-bugs 10 | Please use the template below to create a minimal reproduction 11 | 👉 https://stackblitz.com/github/nuxt/starter/tree/content 12 | - type: textarea 13 | id: bug-env 14 | attributes: 15 | label: Environment 16 | description: You can use `npx nuxi info` to fill this section 17 | placeholder: Environment 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: reproduction 22 | attributes: 23 | label: Reproduction 24 | description: Please provide a link to a repo that can reproduce the problem you ran into. A [**minimal reproduction**](https://v3.nuxtjs.org/community/reporting-bugs#create-a-minimal-reproduction) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "need reproduction" label. If no reproduction is provided we might close it. 25 | placeholder: Reproduction 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: bug-description 30 | attributes: 31 | label: Describe the bug 32 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! 33 | placeholder: Bug description 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: additonal 38 | attributes: 39 | label: Additional context 40 | description: If applicable, add any other context about the problem here` 41 | - type: textarea 42 | id: logs 43 | attributes: 44 | label: Logs 45 | description: | 46 | Optional if provided reproduction. Please try not to insert an image but copy paste the log text. 47 | render: shell 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Module 2 | 3 | > GitHub integration for [Nuxt](https://v3.nuxtjs.org) & [Content](https://content.nuxtjs.org) 4 | 5 | ## Setup 6 | 7 | Install `@nuxtlabs/github-module` in your project: 8 | 9 | ```bash 10 | # Using Yarn 11 | yarn add --dev @nuxtlabs/github-module 12 | # Using NPM 13 | npm install --save-dev @nuxtlabs/github-module 14 | # Using PNPM 15 | pnpm add --save-dev @nuxtlabs/github-module 16 | ``` 17 | 18 | Then, add `@nuxtlabs/github-module` to the `modules` section of your `nuxt.config.ts`: 19 | 20 | ```ts 21 | import { defineNuxtConfig } from 'nuxt' 22 | 23 | export default defineNuxtConfig({ 24 | modules: [ 25 | '@nuxt/content', // Required 26 | '@nuxtlabs/github-module' 27 | ], 28 | github: { 29 | repo: 'nuxt/framework' // Or use GITHUB_REPO in .env 30 | } 31 | }) 32 | ``` 33 | 34 | Lastly, create a [personal access token](https://github.com/settings/tokens) on GitHub and add it into your `.env`: 35 | 36 | ```env 37 | GITHUB_TOKEN='' 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```vue 43 | 46 | 47 | 55 | ``` 56 | 57 | ## Options 58 | 59 | ```ts 60 | github: { 61 | repo: string, 62 | releases: false | { 63 | api: string 64 | repo: string 65 | token: string 66 | /** 67 | * Parse release notes markdown and return AST tree 68 | * 69 | * Note: This option is only available when you have `@nuxt/content` installed in your project. 70 | * 71 | * @default true 72 | */ 73 | parse: boolean 74 | } 75 | } 76 | ``` 77 | 78 | ## Development 79 | 80 | 1. Clone this repository 81 | 2. Install dependencies using `yarn install` or `npm install` 82 | 3. Generate type stubs using `yarn prepare` or `npm run prepare` 83 | 4. In `playground/.env`, add your [personal access token](https://github.com/settings/tokens) 84 | ```env 85 | GITHUB_TOKEN='' 86 | ``` 87 | 5. Launch playground using `yarn dev` or `npm run dev` 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.4.7](https://github.com/nuxtlabs/github-module/compare/v1.4.6...v1.4.7) (2022-08-22) 6 | 7 | 8 | ### Features 9 | 10 | * **GithubLink:** handle contents from remote github source ([#32](https://github.com/nuxtlabs/github-module/issues/32)) ([11f88e7](https://github.com/nuxtlabs/github-module/commit/11f88e7548c96bc2040a6614f3795abe5255c4df)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * add `remark-github` plugin to bundle ([#34](https://github.com/nuxtlabs/github-module/issues/34)) ([9d0236b](https://github.com/nuxtlabs/github-module/commit/9d0236b4de261dc87778afb9ddb13ddd303a4c4d)) 16 | 17 | ### [1.2.4](https://github.com/nuxtlabs/github-module/compare/v1.2.3...v1.2.4) (2022-05-05) 18 | 19 | ### [1.2.3](https://github.com/nuxtlabs/github-module/compare/v1.2.2...v1.2.3) (2022-05-05) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * add `remark-github` to bundle ([31fd022](https://github.com/nuxtlabs/github-module/commit/31fd02295b206de42f443245e01d38709fb2624a)) 25 | 26 | ### [1.2.2](https://github.com/nuxtlabs/github-module/compare/v1.2.1...v1.2.2) (2022-05-02) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * correct package name ([cb7dc49](https://github.com/nuxtlabs/github-module/commit/cb7dc49956182a12814f14a3df4937c645835342)) 32 | * imports ([d871f26](https://github.com/nuxtlabs/github-module/commit/d871f26ab3a7bb24aab37498afde19efbe166a7d)) 33 | 34 | ### [1.2.1](https://github.com/nuxtlabs/github-module/compare/v1.2.0...v1.2.1) (2022-05-02) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * add GITHUB_REPO and use autoimport ([03992ad](https://github.com/nuxtlabs/github-module/commit/03992ad98325456eae0af191fb4f96e2b91e63f5)) 40 | 41 | ## 1.2.0 (2022-05-02) 42 | 43 | 44 | ### Features 45 | 46 | * cache for one minute ([c45a060](https://github.com/nuxtlabs/github-module/commit/c45a060eb336d34131cf67ad635f4deb1e64e944)) 47 | * Nuxt 3 and Content 2 ([#22](https://github.com/nuxtlabs/github-module/issues/22)) ([a1d51a6](https://github.com/nuxtlabs/github-module/commit/a1d51a6b9a86b7d257763d795e3992ca09f3854a)) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * core compatibility ([cb5e792](https://github.com/nuxtlabs/github-module/commit/cb5e792ed202681a7c433b4ca5485bb92ee9787e)) 53 | * module exports ([71d0af8](https://github.com/nuxtlabs/github-module/commit/71d0af856bdd5025fa5cb3e240d7c0773e7baa78)) 54 | * refactor based on new core ([107a251](https://github.com/nuxtlabs/github-module/commit/107a251bbdf8af7ba4616460bec4f080f88e5d86)) 55 | * rename release to releases ([fe924a8](https://github.com/nuxtlabs/github-module/commit/fe924a864691d6aa389fa578cd6ba31544048fa7)) 56 | * use latest core ([cdf354a](https://github.com/nuxtlabs/github-module/commit/cdf354a363aff87843f60da43ac8e377c4a25a82)) 57 | -------------------------------------------------------------------------------- /docs/content/2.configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Configuration" 3 | description: "How to configure the GitHub package." 4 | toc: false 5 | --- 6 | 7 | ::code-group 8 | 9 | ```ts [Minimal config] 10 | export default defineNuxtConfig({ 11 | github: { 12 | owner: 'nuxtlabs', 13 | repo: 'docus', 14 | branch: 'dev' 15 | }, 16 | }) 17 | ``` 18 | 19 | ```ts [Complete config] 20 | export default defineNuxtConfig({ 21 | github: { 22 | // Repository options 23 | owner: 'nuxtlabs', 24 | repo: 'docus', 25 | branch: 'dev', 26 | token: process.env.GITHUB_TOKEN, 27 | api: 'https://api.github.com', 28 | 29 | // Remark plugin (@nuxt/content integration) 30 | remarkPlugin: true, 31 | 32 | contributors: { 33 | // Scoped repository options (optional) 34 | owner: 'nuxtlabs', 35 | repo: 'docus', 36 | branch: 'dev', 37 | token: process.env.GITHUB_TOKEN, 38 | api: 'https://api.github.com', 39 | // Contributors options 40 | max: 100, 41 | }, 42 | 43 | releases: { 44 | // Scoped repository options (optional) 45 | owner: 'nuxtlabs', 46 | repo: 'docus', 47 | branch: 'dev', 48 | token: process.env.GITHUB_TOKEN, 49 | api: 'https://api.github.com', 50 | // Releases options 51 | parse: true, 52 | }, 53 | }, 54 | }) 55 | ``` 56 | 57 | :: 58 | 59 | ::alert 60 | Even if the `GITHUB_TOKEN` environment variable is not set, the GitHub package will still work. 61 | :br :br 62 | We still recommend to specify a `GITHUB_TOKEN`, especially if you are using a `private` repository. 63 | :: 64 | 65 | | **Key** | **Type** | **Default** | **Description** | 66 | | ---------------------------- | --------- | ---------------------- | --------------------------------------------------------- | 67 | | `github.owner` | `string` | | GitHub repository owner | 68 | | `github.repo` | `string` | | GitHub repository name | 69 | | `github.branch` | `string` | main | GitHub repository branch | 70 | | `github.token` | `string` | | GitHub repository token | 71 | | `github.api` | `string` | https://api.github.com | GitHub API URL | 72 | | `github.remarkPlugin` | `boolean` | `false` | Whether or not to use the `@nuxt/content` plugin | 73 | | `github.parseContents ` | `boolean` | `true` | Whether or not to parse content (for instance readme or releases) | 74 | | `github.disableCache` | `boolean` | `false` | Disable cache for data fetched from server routes | 75 | | `github.contributors` | `boolean` | `true` | Allow fetch of contributors data (create server routes) | 76 | | `github.maxContributors` | `number` | `100` | GitHub contributors max number of contributors to display | 77 | | `github.releases` | `boolean` | `true` | Allow fetch of releases data (create server routes) | 78 | -------------------------------------------------------------------------------- /.github/scripts/bump-edge.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'fs' 2 | import { execSync } from 'child_process' 3 | import { resolve } from 'pathe' 4 | import { globby } from 'globby' 5 | 6 | // Temporary forked from nuxt/framework 7 | 8 | async function loadPackage (dir: string) { 9 | const pkgPath = resolve(dir, 'package.json') 10 | const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}')) 11 | const save = () => fsp.writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n') 12 | 13 | const updateDeps = (reviver: Function) => { 14 | for (const type of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) { 15 | if (!data[type]) { continue } 16 | for (const e of Object.entries(data[type])) { 17 | const dep = { name: e[0], range: e[1], type } 18 | delete data[type][dep.name] 19 | const updated = reviver(dep) || dep 20 | data[updated.type] = data[updated.type] || {} 21 | data[updated.type][updated.name] = updated.range 22 | } 23 | } 24 | } 25 | 26 | return { 27 | dir, 28 | data, 29 | save, 30 | updateDeps 31 | } 32 | } 33 | 34 | type ThenArg = T extends PromiseLike ? U : T 35 | type Package = ThenArg> 36 | 37 | async function loadWorkspace (dir: string) { 38 | const workspacePkg = await loadPackage(dir) 39 | const pkgDirs = await globby(workspacePkg.data.workspaces || [], { onlyDirectories: true }) 40 | 41 | const packages: Package[] = [workspacePkg] 42 | 43 | for (const pkgDir of pkgDirs) { 44 | const pkg = await loadPackage(pkgDir) 45 | if (!pkg.data.name) { continue } 46 | packages.push(pkg) 47 | } 48 | 49 | const find = (name: string) => { 50 | const pkg = packages.find(pkg => pkg.data.name === name) 51 | if (!pkg) { 52 | throw new Error('Workspace package not found: ' + name) 53 | } 54 | return pkg 55 | } 56 | 57 | const rename = (from: string, to: string) => { 58 | find(from).data.name = to 59 | for (const pkg of packages) { 60 | pkg.updateDeps((dep) => { 61 | if (dep.name === from && !dep.range.startsWith('npm:')) { 62 | dep.range = 'npm:' + to + '@' + dep.range 63 | } 64 | }) 65 | } 66 | } 67 | 68 | const setVersion = (name: string, newVersion: string) => { 69 | find(name).data.version = newVersion 70 | for (const pkg of packages) { 71 | pkg.updateDeps((dep) => { 72 | if (dep.name === name) { 73 | dep.range = newVersion 74 | } 75 | }) 76 | } 77 | } 78 | 79 | const save = () => Promise.all(packages.map(pkg => pkg.save())) 80 | 81 | return { 82 | dir, 83 | workspacePkg, 84 | packages, 85 | save, 86 | find, 87 | rename, 88 | setVersion 89 | } 90 | } 91 | 92 | async function main () { 93 | const workspace = await loadWorkspace(process.cwd()) 94 | 95 | const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim() 96 | const date = Math.round(Date.now() / (1000 * 60)) 97 | 98 | for (const pkg of workspace.packages.filter(p => !p.data.private)) { 99 | workspace.setVersion(pkg.data.name, `${pkg.data.version}-${date}.${commit}`) 100 | workspace.rename(pkg.data.name, pkg.data.name + '-edge') 101 | } 102 | 103 | await workspace.save() 104 | } 105 | 106 | main().catch((err) => { 107 | // eslint-disable-next-line no-console 108 | console.error(err) 109 | process.exit(1) 110 | }) 111 | -------------------------------------------------------------------------------- /src/runtime/types/repository.d.ts: -------------------------------------------------------------------------------- 1 | export interface GithubRepositoryOptions { 2 | owner?: string 3 | branch?: string 4 | repo?: string 5 | api?: string 6 | token?: string 7 | } 8 | 9 | export interface GithubRepositoryReadme { 10 | name: string 11 | path: string 12 | sha: string 13 | size: number 14 | url: string 15 | html_url: string 16 | git_url: string 17 | download_url: string 18 | type: 'file' 19 | content: string, 20 | encoding: 'base64' 21 | } 22 | 23 | export interface GithubRepositoryOwner { 24 | login: string 25 | id: number 26 | node_id: string 27 | avatar_url: string 28 | gravatar_id: string 29 | url: string 30 | html_url: string 31 | followers_url: string 32 | following_url: string 33 | gists_url: string 34 | starred_url: string 35 | subscriptions_url: string 36 | organizations_url: string 37 | repos_url: string 38 | events_url: string 39 | received_events_url: string 40 | type: string 41 | site_admin: boolean 42 | } 43 | 44 | export interface GithubRepositoryLicense { 45 | key: string 46 | name: string 47 | spdx_id: string 48 | url: string 49 | node_id: string 50 | } 51 | 52 | export interface GithubRepositoryOrganization { 53 | login: string 54 | id: number 55 | node_id: string 56 | avatar_url: string 57 | gravatar_id: string 58 | url: string 59 | html_url: string 60 | followers_url: string 61 | following_url: string 62 | gists_url: string 63 | starred_url: string 64 | subscriptions_url: string 65 | organizations_url: string 66 | repos_url: string 67 | events_url: string 68 | received_events_url: string 69 | type: string 70 | site_admin: boolean 71 | } 72 | 73 | export interface GithubRepository { 74 | id: number 75 | node_id: string 76 | name: string 77 | full_name: string 78 | private: boolean 79 | owner: GithubRepositoryOwner 80 | html_url: string 81 | description: string 82 | fork: boolean 83 | url: string 84 | forks_url: string 85 | keys_url: string 86 | collaborators_url: string 87 | teams_url: string 88 | hooks_url: string 89 | issue_events_url: string 90 | events_url: string 91 | assignees_url: string 92 | branches_url: string 93 | tags_url: string 94 | blobs_url: string 95 | git_tags_url: string 96 | git_refs_url: string 97 | trees_url: string 98 | statuses_url: string 99 | languages_url: string 100 | stargazers_url: string 101 | contributors_url: string 102 | subscribers_url: string 103 | subscription_url: string 104 | commits_url: string 105 | git_commits_url: string 106 | comments_url: string 107 | issue_comment_url: string 108 | contents_url: string 109 | compare_url: string 110 | merges_url: string 111 | archive_url: string 112 | downloads_url: string 113 | issues_url: string 114 | pulls_url: string 115 | milestones_url: string 116 | notifications_url: string 117 | labels_url: string 118 | releases_url: string 119 | deployments_url: string 120 | created_at: Date 121 | updated_at: Date 122 | pushed_at: Date 123 | git_url: string 124 | ssh_url: string 125 | clone_url: string 126 | svn_url: string 127 | homepage: string 128 | size: number 129 | stargazers_count: number 130 | watchers_count: number 131 | language: string 132 | has_issues: boolean 133 | has_projects: boolean 134 | has_downloads: boolean 135 | has_wiki: boolean 136 | has_pages: boolean 137 | forks_count: number 138 | mirror_url?: any 139 | archived: boolean 140 | disabled: boolean 141 | open_issues_count: number 142 | license: GithubRepositoryLicense 143 | allow_forking: boolean 144 | is_template: boolean 145 | web_commit_signoff_required: boolean 146 | topics: string[] 147 | visibility: string 148 | forks: number 149 | open_issues: number 150 | watchers: number 151 | default_branch: string 152 | temp_clone_token?: any 153 | organization: GithubRepositoryOrganization 154 | network_count: number 155 | subscribers_count: number 156 | } 157 | -------------------------------------------------------------------------------- /src/runtime/components/GithubLink.ts: -------------------------------------------------------------------------------- 1 | import { joinURL } from 'ufo' 2 | import type { PropType } from 'vue' 3 | import { computed, defineComponent, useSlots } from 'vue' 4 | // @ts-ignore 5 | import { useRuntimeConfig } from '#imports' 6 | 7 | export default defineComponent({ 8 | props: { 9 | /** 10 | * Repository owner. 11 | */ 12 | owner: { 13 | type: String, 14 | default: () => useRuntimeConfig()?.github?.owner, 15 | required: false 16 | }, 17 | /** 18 | * Repository name. 19 | */ 20 | repo: { 21 | type: String, 22 | default: () => useRuntimeConfig()?.github?.repo, 23 | required: false 24 | }, 25 | /** 26 | * The branch to use for the edit link. 27 | */ 28 | branch: { 29 | type: String, 30 | default: () => useRuntimeConfig()?.github?.branch, 31 | required: false 32 | }, 33 | /** 34 | * A base directory to append to the source path. 35 | * 36 | * Won't be used if `page` is set. 37 | */ 38 | dir: { 39 | type: String, 40 | default: () => useRuntimeConfig()?.github?.dir, 41 | required: false 42 | }, 43 | /** 44 | * Source file path. 45 | * 46 | * Won't be used if `page` is set. 47 | */ 48 | source: { 49 | type: String, 50 | required: false, 51 | default: undefined 52 | }, 53 | /** 54 | * Use page from @nuxt/content. 55 | */ 56 | page: { 57 | type: Object as PropType, 58 | required: false, 59 | default: undefined 60 | }, 61 | /** 62 | * Content directory (to be used with `page`) 63 | */ 64 | contentDir: { 65 | type: String, 66 | required: false, 67 | default: 'content' 68 | }, 69 | /** 70 | * Send to an edit page or not. 71 | */ 72 | edit: { 73 | type: Boolean, 74 | required: false, 75 | default: true 76 | } 77 | }, 78 | setup (props) { 79 | if (!props.owner || !props.repo || !props.branch) { 80 | throw new Error('If you want to use `GithubLink` component, you must specify: `owner`, `repo` and `branch`.') 81 | } 82 | 83 | const source = computed(() => { 84 | let { repo, owner, branch, contentDir } = props 85 | let prefix = '' 86 | 87 | // Resolve source from content sources 88 | if (useRuntimeConfig()?.public?.content) { 89 | let source 90 | const { sources } = useRuntimeConfig().public.content 91 | 92 | for (const key in sources || []) { 93 | if (props.page._id.startsWith(key)) { 94 | source = sources[key] 95 | break 96 | } 97 | } 98 | 99 | if (source?.driver === 'github') { 100 | repo = source.repo || props.repo || '' 101 | owner = source.owner || props.owner || '' 102 | branch = source.branch || props.branch || 'main' 103 | contentDir = source.dir || props.contentDir || '' 104 | prefix = source.prefix || '' 105 | } 106 | } 107 | 108 | return { repo, owner, branch, contentDir, prefix } 109 | }) 110 | 111 | const base = computed(() => joinURL('https://github.com', `${source.value.owner}/${source.value.repo}`)) 112 | 113 | const path = computed(() => { 114 | const dirParts: string[] = [] 115 | 116 | // @nuxt/content support 117 | // Create the URL from a document data. 118 | if (props?.page?._path) { 119 | // Use content dir 120 | if (source.value.contentDir) { dirParts.push(source.value.contentDir) } 121 | 122 | // Get page file from page data 123 | dirParts.push(props.page._file.substring(source.value.prefix.length)) 124 | 125 | return dirParts 126 | } 127 | 128 | // Use props dir 129 | if (props.dir) { 130 | dirParts.push(props.dir) 131 | } 132 | 133 | // Use props source 134 | if (props.source) { 135 | dirParts.push(props.source) 136 | } 137 | 138 | return dirParts 139 | }) 140 | 141 | /** 142 | * Create edit link. 143 | */ 144 | const url = computed(() => { 145 | const parts = [base.value] 146 | 147 | if (props.edit) { parts.push('edit') } else { parts.push('tree') } 148 | 149 | parts.push(source.value.branch, ...path.value) 150 | 151 | return parts.filter(Boolean).join('/') 152 | }) 153 | 154 | return { 155 | url 156 | } 157 | }, 158 | render (ctx) { 159 | const { url } = ctx 160 | 161 | const slots = useSlots() 162 | 163 | return slots?.default?.({ url }) 164 | } 165 | }) 166 | -------------------------------------------------------------------------------- /docs/content/3.components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Components" 3 | description: "Discover every component from the GitHub package." 4 | --- 5 | 6 | This page uses [`@nuxt/content`](https://github.com/nuxt/content) repository as filling data for the releases. 7 | 8 | ## `` 9 | 10 | This component is useful if you want to display a link to the GitHub repository. 11 | 12 | Can be used for both browsing and editing links. 13 | 14 | ::code-group 15 | 16 | ::code-block{label="Preview"} 17 | ::source-link 18 | --- 19 | source: "packages/github/src/runtime/components/GithubLink.ts" 20 | --- 21 | :: 22 | :: 23 | 24 | ```vue [Code] 25 | 30 | ``` 31 | 32 | :: 33 | 34 | ::props{of="GithubLink"} 35 | :: 36 | 37 | ::source-link 38 | --- 39 | source: "packages/github/src/runtime/components/GithubLink.ts" 40 | --- 41 | :: 42 | 43 | --- 44 | 45 | ## `` 46 | 47 | This component is useful if you want to a repository contributors. 48 | 49 | ::code-group 50 | 51 | ::code-block{label="Preview"} 52 | ::contributors-example 53 | :: 54 | :: 55 | 56 | ```vue [Code] 57 | 64 | ``` 65 | 66 | :: 67 | 68 | ::props{of="GithubContributors"} 69 | :: 70 | 71 | ::source-link 72 | --- 73 | source: "packages/github/src/runtime/components/GithubContributors.ts" 74 | --- 75 | :: 76 | 77 | --- 78 | 79 | ## `` 80 | 81 | This component is useful if you want to a file contributors. 82 | 83 | ::code-group 84 | 85 | ::code-block{label="Preview"} 86 | ::file-contributors-example 87 | :: 88 | :: 89 | 90 | ```vue [Code] 91 | 94 | 95 | 102 | ``` 103 | 104 | :: 105 | 106 | ::props{of="GithubFileContributors"} 107 | :: 108 | 109 | ::source-link 110 | --- 111 | source: "packages/github/src/runtime/components/GithubFileContributors.ts" 112 | --- 113 | :: 114 | 115 | --- 116 | 117 | ## `` 118 | 119 | This component is useful if you want to display last release of the repository. 120 | 121 | ::code-group 122 | 123 | ::code-block{label="Preview"} 124 | ::div{class="max-h-[300px] pb-8"} 125 | ::last-release-example 126 | :: 127 | :: 128 | :: 129 | 130 | ```vue [Code] 131 | 137 | ``` 138 | 139 | :: 140 | 141 | ::props{of="GithubLastRelease"} 142 | :: 143 | 144 | ::source-link 145 | --- 146 | source: "packages/github/src/runtime/components/GithubLastRelease.ts" 147 | --- 148 | :: 149 | 150 | --- 151 | 152 | ## `` 153 | 154 | This component is useful if you want to display release fetched by tag. 155 | 156 | ::code-group 157 | 158 | ::code-block{label="Preview"} 159 | ::div{class="max-h-[300px] pb-8"} 160 | ::release-example 161 | :: 162 | :: 163 | :: 164 | 165 | ```vue [Code] 166 | 172 | ``` 173 | 174 | :: 175 | 176 | ::props{of="GithubRelease"} 177 | :: 178 | 179 | ::source-link 180 | --- 181 | source: "packages/github/src/runtime/components/GithubRelease.ts" 182 | --- 183 | :: 184 | 185 | --- 186 | 187 | ## `` 188 | 189 | This component is useful if you want to display all the releases of the repository. 190 | 191 | ::code-group 192 | 193 | ::code-block{label="Preview"} 194 | ::div{class="max-h-[300px] pb-8"} 195 | ::releases-example 196 | :: 197 | :: 198 | :: 199 | 200 | ```vue [Code] 201 | 209 | ``` 210 | 211 | :: 212 | 213 | ::props{of="GithubReleases"} 214 | :: 215 | 216 | ::source-link 217 | --- 218 | source: "packages/github/src/runtime/components/GithubReleases.ts" 219 | --- 220 | :: 221 | 222 | --- 223 | 224 | ## `` 225 | 226 | This component is useful if you want to display all the informations of the repository. 227 | 228 | ::code-group 229 | 230 | ::code-block{label="Preview"} 231 | ::div{class="max-h-[300px] pb-8"} 232 | ::repository-example 233 | :: 234 | :: 235 | :: 236 | 237 | ```vue [Code] 238 | 243 | ``` 244 | 245 | :: 246 | 247 | ::props{of="GithubRepository"} 248 | :: 249 | 250 | ::source-link 251 | --- 252 | source: "packages/github/src/runtime/components/GithubRepository.ts" 253 | --- 254 | :: 255 | 256 | ## `` 257 | 258 | This component helps to display the content of `README.md` in any repository. 259 | 260 | ::code-group 261 | 262 | ::code-block{label="Preview"} 263 | ::div{class="max-h-[300px] pb-8"} 264 | ::readme-example 265 | :: 266 | :: 267 | :: 268 | 269 | ```vue [Code] 270 | 275 | ``` 276 | 277 | :: 278 | 279 | ::props{of="GithubReadme"} 280 | :: 281 | 282 | ::source-link 283 | --- 284 | source: "packages/github/src/runtime/components/GithubReadme.ts" 285 | --- 286 | :: 287 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { defu } from 'defu' 2 | import { addImports, addComponent, createResolver, defineNuxtModule, resolveModule } from '@nuxt/kit' 3 | import type { GithubRepositoryOptions } from './runtime/types' 4 | 5 | export interface ModuleOptions extends GithubRepositoryOptions { 6 | disableCache?: boolean 7 | remarkPlugin?: boolean 8 | releases?: boolean 9 | contributors?: boolean 10 | maxContributors?: number 11 | /** 12 | * Parse contents (releases content, readme) Markdown and return AST tree. 13 | * 14 | * Note: This option is only available when you have `@nuxt/content` installed in your project. 15 | * 16 | * @default true 17 | */ 18 | parseContents?: boolean 19 | } 20 | 21 | declare module '@nuxt/schema' { 22 | interface PublicRuntimeConfig { 23 | // @ts-ignore 24 | github?: ModuleOptions; 25 | } 26 | 27 | interface RuntimeConfig { 28 | // @ts-ignore 29 | github?: ModuleOptions; 30 | } 31 | } 32 | 33 | export default defineNuxtModule({ 34 | meta: { 35 | name: '@nuxtlabs/github-module', 36 | configKey: 'github' 37 | }, 38 | defaults: { 39 | owner: '', 40 | repo: '', 41 | token: undefined, 42 | branch: 'main', 43 | api: 'https://api.github.com', 44 | remarkPlugin: true, 45 | disableCache: false, 46 | releases: true, 47 | contributors: true, 48 | maxContributors: 100, 49 | parseContents: true 50 | }, 51 | setup (options, nuxt) { 52 | const { resolve } = createResolver(import.meta.url) 53 | const runtimeDir = resolve('./runtime') 54 | 55 | if (!options.owner) { 56 | // Check if we can split repo name into owner/repo 57 | if (options.repo && options.repo.includes('/')) { 58 | const [owner, repo] = options.repo.split('/') 59 | options.owner = owner 60 | options.repo = repo 61 | } 62 | } 63 | 64 | // @ts-ignore 65 | if (!nuxt.options.runtimeConfig.public) { nuxt.options.runtimeConfig.public = {} } 66 | 67 | const config = { 68 | api: options.api || process.env.GITHUB_OWNER, 69 | owner: options.owner || process.env.GITHUB_OWNER, 70 | branch: options.branch || process.env.GITHUB_BRANCH, 71 | repo: options.repo || process.env.GITHUB_REPO, 72 | disableCache: options.disableCache, 73 | parseContents: options.parseContents, 74 | maxContributors: options.maxContributors 75 | } 76 | 77 | // Public runtime config 78 | // @ts-ignore 79 | nuxt.options.runtimeConfig.public.github = defu(nuxt.options.runtimeConfig.public.github, config) 80 | // @ts-ignore 81 | nuxt.options.runtimeConfig.github = defu(nuxt.options.runtimeConfig.github, { 82 | token: options.token || process.env.GITHUB_TOKEN 83 | }, config) 84 | 85 | // Autolink issue/PR/commit links using `remark-github` plugin 86 | if (options.remarkPlugin) { 87 | // @ts-ignore 88 | nuxt.hook('content:context', (context) => { 89 | context.markdown.remarkPlugins ??= {} 90 | 91 | if (!Array.isArray(context.markdown.remarkPlugins)) { 92 | context.markdown.remarkPlugins['remark-github'] = { repository: `${options.owner}/${options.repo}` } 93 | } 94 | }) 95 | } 96 | 97 | const nitroConfig = nuxt.options.nitro 98 | 99 | // Init Nitro handlers 100 | nitroConfig.handlers = nitroConfig.handlers || [] 101 | nitroConfig.prerender = nitroConfig.prerender || {} 102 | nitroConfig.prerender.routes = nitroConfig.prerender.routes || [] 103 | 104 | // Setup repository API 105 | nitroConfig.handlers.push({ 106 | route: '/api/_github/repository/:query', 107 | handler: resolveModule('./server/api/repository', { paths: runtimeDir }) 108 | }) 109 | 110 | // Setup readme file API 111 | nitroConfig.handlers.push({ 112 | route: '/api/_github/readme/:query', 113 | handler: resolveModule('./server/api/readme', { paths: runtimeDir }) 114 | }) 115 | 116 | // Repository component 117 | addComponent({ 118 | name: 'GithubRepository', 119 | filePath: resolveModule('./components/GithubRepository', { paths: runtimeDir }), 120 | global: true 121 | }) 122 | 123 | // GithubLink component 124 | addComponent({ 125 | name: 'GithubLink', 126 | filePath: resolveModule('./components/GithubLink', { paths: runtimeDir }), 127 | global: true 128 | }) 129 | 130 | // GithubReadme component 131 | addComponent({ 132 | name: 'GithubReadme', 133 | filePath: resolveModule('./components/GithubReadme', { paths: runtimeDir }), 134 | global: true 135 | }) 136 | 137 | // Setup releases API 138 | if (options.releases) { 139 | // Releases list 140 | nitroConfig.handlers.push({ 141 | route: '/api/_github/releases/:query', 142 | handler: resolveModule('./server/api/releases/index', { paths: runtimeDir }) 143 | }) 144 | nitroConfig.prerender.routes.push('/api/_github/releases/index.json') 145 | 146 | // Releases components 147 | addComponent({ 148 | name: 'GithubReleases', 149 | filePath: resolveModule('./components/GithubReleases', { paths: runtimeDir }), 150 | global: true 151 | }) 152 | addComponent({ 153 | name: 'GithubLastRelease', 154 | filePath: resolveModule('./components/GithubLastRelease', { paths: runtimeDir }), 155 | global: true 156 | }) 157 | addComponent({ 158 | name: 'GithubRelease', 159 | filePath: resolveModule('./components/GithubRelease', { paths: runtimeDir }), 160 | global: true 161 | }) 162 | } 163 | 164 | // Setup contributors API 165 | if (options.contributors) { 166 | nitroConfig.handlers.push({ 167 | route: '/api/_github/contributors/:query', 168 | handler: resolveModule('./server/api/contributors', { paths: runtimeDir }) 169 | }) 170 | nitroConfig.prerender.routes.push('/api/_github/contributors/index.json') 171 | 172 | // TODO: Add prerender for file arguments (using :source argument) 173 | nitroConfig.handlers.push({ 174 | route: '/api/_github/contributors/file/:query', 175 | handler: resolveModule('./server/api/contributors/file', { paths: runtimeDir }) 176 | }) 177 | 178 | // Contributors components 179 | addComponent({ 180 | name: 'GithubContributors', 181 | filePath: resolveModule('./components/GithubContributors', { paths: runtimeDir }), 182 | global: true 183 | }) 184 | addComponent({ 185 | name: 'GithubFileContributors', 186 | filePath: resolveModule('./components/GithubFileContributors', { paths: runtimeDir }), 187 | global: true 188 | }) 189 | } 190 | 191 | // Setup commits API 192 | nitroConfig.handlers.push({ 193 | route: '/api/_github/commits/:query', 194 | handler: resolveModule('./server/api/commits', { paths: runtimeDir }) 195 | }) 196 | 197 | // GithubCommits component 198 | addComponent({ 199 | name: 'GithubCommits', 200 | filePath: resolveModule('./components/GithubCommits', { paths: runtimeDir }), 201 | global: true 202 | }) 203 | 204 | addImports({ 205 | name: 'useGithub', 206 | from: resolveModule('./composables/useGithub', { paths: runtimeDir }) 207 | }) 208 | 209 | nitroConfig.externals = defu(typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, { 210 | inline: [ 211 | // Inline module runtime in Nitro bundle 212 | resolve('./runtime') 213 | ] 214 | }) 215 | } 216 | }) 217 | -------------------------------------------------------------------------------- /src/runtime/server/utils/queries.ts: -------------------------------------------------------------------------------- 1 | import { joinURL, withQuery, QueryObject } from 'ufo' 2 | import { graphql } from '@octokit/graphql' 3 | import remarkGithub from 'remark-github' 4 | import { defu } from 'defu' 5 | import type { ModuleOptions } from '../../../module' 6 | import type { GithubRawRelease, GithubRepositoryOptions, GithubRawContributor, GithubContributorsQuery, GithubReleasesQuery, GithubRepositoryReadme, GithubRepository, GithubCommitsQuery, GithubAuthor } from '../../types' 7 | 8 | export function decodeParams (params = '') { 9 | const result: Record = {} 10 | params = params.replace(/\.json$/, '') 11 | for (const param of params.split(':')) { 12 | const [key, ...value] = param.split('_') 13 | result[key] = value.join('_') 14 | } 15 | return result 16 | } 17 | 18 | function isBot (user: GithubAuthor) { 19 | return user.login.includes('[bot]') || user.login.includes('-bot') || user.login.includes('.bot') 20 | } 21 | 22 | function normalizeRelease (release: any): GithubRawRelease { 23 | return { 24 | name: normalizeReleaseName(release?.name || release?.tag_name), 25 | tag_name: release?.tag_name, 26 | date: release?.published_at, 27 | body: release?.body, 28 | v: +normalizeReleaseName(release?.tag_name)?.substring(1, 2) || 0, 29 | url: release?.html_url, 30 | tarball: release?.tarball_url, 31 | zipball: release?.zipball_url, 32 | prerelease: release?.prerelease, 33 | reactions: release?.reactions, 34 | author: { 35 | name: release?.author?.login, 36 | url: release?.author?.html_url, 37 | avatar: release?.author?.avatar_url 38 | } 39 | } 40 | } 41 | 42 | function normalizeReleaseName (name: string) { 43 | if (!name) { return '' } 44 | 45 | // remove "Release " prefix from release name 46 | name = name.replace('Release ', '') 47 | 48 | // make sure release name starts with an alphabetical character 49 | if (!name.match(/^[a-zA-Z]/)) { 50 | name = `v${name}` 51 | } 52 | 53 | return name 54 | } 55 | 56 | export function githubGraphqlQuery (query: string, options: Partial): Promise { 57 | const gq = graphql.defaults({ 58 | headers: { 59 | authorization: `token ${options.token}` 60 | } 61 | }) 62 | 63 | return gq(query) 64 | } 65 | 66 | export const parseRelease = async (release: GithubRawRelease, githubConfig: GithubRepositoryOptions) => { 67 | // @ts-ignore 68 | const markdown = await import('@nuxt/content/transformers/markdown').then(m => m.default || m) 69 | let parsedRelease 70 | try { 71 | parsedRelease = { 72 | ...release, 73 | // Parse release notes when `@nuxt/content` is installed. 74 | ...( 75 | release?.body && release?.name 76 | ? await markdown.parse(`github:${release.name}.md`, release.body, { 77 | remarkPlugins: { 78 | 'remark-github': { 79 | instance: remarkGithub, 80 | repository: `${githubConfig.owner}/${githubConfig.repo}` 81 | } 82 | } 83 | }) 84 | : {} 85 | ) 86 | } 87 | } catch (_err: any) { 88 | // eslint-disable-next-line no-console 89 | console.warn(`Cannot parse release ${release?.name} [${_err.response?.status || 500}]`) 90 | return 91 | } 92 | 93 | return parsedRelease 94 | } 95 | 96 | export function overrideConfig (config: ModuleOptions, query: GithubRepositoryOptions): GithubRepositoryOptions { 97 | return (({ owner, repo, branch, api, token }) => ({ owner, repo, branch, api, token }))(defu(query, config)) 98 | } 99 | 100 | export async function fetchRepository ({ api, owner, repo, token }: GithubRepositoryOptions) { 101 | const url = `${api}/repos/${owner}/${repo}` 102 | 103 | const repository = await $fetch(url, { 104 | headers: token ? { Authorization: `token ${token}` } : {} 105 | }).catch((_) => { 106 | /* 107 | // eslint-disable-next-line no-console 108 | console.warn(`Cannot fetch GitHub Repository on ${url} [${err.response?.status || 500}]`) 109 | 110 | // eslint-disable-next-line no-console 111 | console.info('If your repository is private, make sure to provide GITHUB_TOKEN environment in `.env`') 112 | */ 113 | return {} 114 | }) 115 | 116 | return repository 117 | } 118 | 119 | export async function fetchRepositoryContributors ({ max }: Partial, { api, owner, repo, token }: GithubRepositoryOptions) { 120 | let url = `${api}/repos/${owner}/${repo}/contributors` 121 | 122 | url = withQuery(url, { max } as QueryObject) 123 | 124 | const contributors = await $fetch>(url, { 125 | headers: token ? { Authorization: `token ${token}` } : {} 126 | }).catch((_) => { 127 | /* 128 | // eslint-disable-next-line no-console 129 | console.warn(`Cannot fetch GitHub contributors on ${url} [${err.response?.status || 500}]`) 130 | 131 | // eslint-disable-next-line no-console 132 | console.info('If your repository is private, make sure to provide GITHUB_TOKEN environment in `.env`') 133 | 134 | if (err?.response?.status !== 403) { 135 | // eslint-disable-next-line no-console 136 | console.info('To disable fetching contributors, set `github.contributors` to `false` in `nuxt.config.ts`') 137 | } 138 | */ 139 | return [] 140 | }) 141 | 142 | // eslint-disable-next-line camelcase 143 | return contributors.map(({ avatar_url, login }) => ({ avatar_url, login })) 144 | } 145 | 146 | export async function fetchCommits ({ date, source }: Partial & { date: Date }>, { owner, repo, branch, token }: GithubRepositoryOptions) { 147 | const daysAgo = () => { 148 | if (date) { return date.toISOString() } 149 | 150 | const now = new Date() 151 | now.setDate(now.getDate() - 30) // get from 30 days ago 152 | return now.toISOString() 153 | } 154 | 155 | const path = source ? `path: "${source}",` : '' 156 | const data = await githubGraphqlQuery( 157 | ` 158 | query { 159 | repository(owner: "${owner}", name: "${repo}") { 160 | object(expression: "${branch}") { 161 | ... on Commit { 162 | history(since: "${daysAgo()}", ${path}) { 163 | nodes { 164 | oid 165 | messageHeadlineHTML 166 | authors(first: ${5}) { 167 | nodes { 168 | user { 169 | name 170 | avatarUrl 171 | login 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | } 181 | `, { token } 182 | ) 183 | 184 | if (!data?.repository?.object?.history?.nodes) { return [] } 185 | 186 | const commits = data.repository.object.history.nodes.map((node: any) => ({ 187 | hash: node.oid, 188 | message: node.messageHeadlineHTML, 189 | authors: node.authors.nodes 190 | .map((author: any) => author.user) 191 | .filter((user: GithubAuthor) => user?.name && !isBot(user)) 192 | })) 193 | 194 | return commits 195 | } 196 | 197 | export async function fetchFileContributors ({ source, max }: Partial, { owner, repo, branch, token }: GithubRepositoryOptions & { maxContributors?: number }) { 198 | const data = await githubGraphqlQuery( 199 | ` 200 | query { 201 | repository(owner: "${owner}", name: "${repo}") { 202 | object(expression: "${branch}") { 203 | ... on Commit { 204 | history(first: ${max}, path: "${source}") { 205 | nodes { 206 | authors(first: ${max}) { 207 | nodes { 208 | user { 209 | name 210 | avatarUrl 211 | login 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | }`, 221 | { token } 222 | ).catch((_) => { 223 | /* 224 | // eslint-disable-next-line no-console 225 | console.warn(`Cannot fetch GitHub file contributors on ${source} [${err.response?.status || 500}]`) 226 | 227 | // eslint-disable-next-line no-console 228 | console.info('If your repository is private, make sure to provide GITHUB_TOKEN environment in `.env`') 229 | 230 | if (err?.response?.status !== 403) { 231 | // eslint-disable-next-line no-console 232 | console.info('To disable fetching contributors, set `github.contributors` to `false` in `nuxt.config.ts`') 233 | } 234 | */ 235 | }) 236 | 237 | if (!data?.repository?.object?.history?.nodes) { return [] } 238 | 239 | let users: GithubAuthor[] = data.repository.object.history.nodes 240 | .map((node: any) => node.authors.nodes) 241 | .flat() 242 | .map((node: any) => node.user) 243 | .filter((user: GithubAuthor) => user && user.name) 244 | .filter((user: GithubAuthor) => !isBot(user)) 245 | 246 | // Unique 247 | users = users.reduce((unique: GithubAuthor[], user: GithubAuthor) => (unique.find(u => u.login === user.login) ? unique : unique.concat(user)), []) 248 | 249 | return users.map(({ avatarUrl, name, login }) => ({ avatar_url: avatarUrl, name, login })) 250 | } 251 | 252 | export async function fetchReleases (query: Partial, { api, repo, token, owner }: GithubRepositoryOptions) { 253 | const page = query?.page || 1 254 | const perPage = query?.per_page || 100 255 | const last = query?.last || false 256 | const tag = query?.tag || false 257 | 258 | let url = `${api}/repos/${owner}/${repo}/releases` 259 | if (tag) { 260 | url = joinURL(url, 'tags', tag) 261 | } else if (last) { 262 | url = joinURL(url, 'latest') 263 | } else { 264 | url = withQuery(url, { per_page: perPage, page } as any) 265 | } 266 | 267 | const rawReleases = await $fetch>(url, { 268 | headers: token ? { Authorization: `token ${token}` } : {} 269 | }).catch((_) => { 270 | /* 271 | // eslint-disable-next-line no-console 272 | console.warn(`Cannot fetch GitHub releases on ${url} [${err.response?.status || 500}]`) 273 | 274 | // eslint-disable-next-line no-console 275 | console.info('If your repository is private, make sure to provide GITHUB_TOKEN environment in `.env`') 276 | 277 | if (err.response.status !== 403) { 278 | // eslint-disable-next-line no-console 279 | console.info('To disable fetching releases, set `github.releases` to `false` in `nuxt.config.ts`') 280 | } 281 | */ 282 | }) 283 | 284 | if (!rawReleases) { return last ? {} : [] } 285 | 286 | return (last || tag) ? normalizeRelease(rawReleases) : rawReleases.filter((r: any) => !r.draft).map(normalizeRelease) 287 | } 288 | 289 | export async function fetchReadme ({ api, owner, repo, token }: GithubRepositoryOptions) { 290 | const url = `${api}/repos/${owner}/${repo}/readme` 291 | 292 | const readme = await $fetch(url, { 293 | headers: token ? { Authorization: `token ${token}` } : {} 294 | }).catch((_) => { 295 | /* 296 | // eslint-disable-next-line no-console 297 | console.warn(`Cannot fetch GitHub readme on ${url} [${err.response?.status || 500}]`) 298 | 299 | // eslint-disable-next-line no-console 300 | console.info('If your repository is private, make sure to provide GITHUB_TOKEN environment in `.env`') 301 | */ 302 | 303 | return {} 304 | }) 305 | 306 | return readme 307 | } 308 | --------------------------------------------------------------------------------