mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
WIP
This commit is contained in:
parent
f121657fc1
commit
1cd5ba57e4
9 changed files with 648 additions and 12 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { errors } from 'platforms'
|
||||
import { isEnterprise } from '.'
|
||||
import { is } from '../../utils/is'
|
||||
import { gitakoServiceHost } from '../../utils/networkService'
|
||||
import { gitakoServiceHost, responseBodyResolvers } from '../../utils/networkService'
|
||||
import { continuousLoadPages, getDOM, resolveHeaderLink } from './utils'
|
||||
|
||||
function isAPIRateLimitExceeded(content: JSONValue) {
|
||||
|
|
@ -19,15 +19,6 @@ function isBlockedProject(content: JSONValue) {
|
|||
return is.JSON.object(content) && content?.['message'] === 'Repository access blocked'
|
||||
}
|
||||
|
||||
export const responseBodyResolvers = {
|
||||
asIs: (response: Response) => response,
|
||||
json(response: Response) {
|
||||
const contentType = response.headers.get('Content-Type') || response.headers.get('content-type')
|
||||
if (contentType?.includes('application/json')) return response.json()
|
||||
throw new Error(`Response content type is "${contentType}"`)
|
||||
},
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
url: string,
|
||||
{
|
||||
|
|
@ -203,7 +194,10 @@ export async function requestCommitTreeData(
|
|||
return await request(url, { accessToken }, responseBodyResolvers.asIs)
|
||||
}
|
||||
|
||||
export async function getPaginatedData<T>(sendRequest: (page: number) => Promise<Response>) {
|
||||
export async function getPaginatedData<T>(
|
||||
// TODO: refactor, update sendRequest arguments with URLs in response header `link`
|
||||
sendRequest: (page: number) => Promise<Response>,
|
||||
) {
|
||||
const responses: Response[] = []
|
||||
let page = 1
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
|
|
@ -237,5 +231,5 @@ export async function getPaginatedData<T>(sendRequest: (page: number) => Promise
|
|||
break
|
||||
}
|
||||
}
|
||||
return Promise.all(responses.map(responseBodyResolvers.json)) as Promise<T[]>
|
||||
return Promise.all(responses.map(responseBodyResolvers.json) as T[])
|
||||
}
|
||||
|
|
|
|||
163
src/platforms/GitLab/API.ts
Normal file
163
src/platforms/GitLab/API.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { errors } from 'platforms'
|
||||
import { is } from 'utils/is'
|
||||
import {
|
||||
gitakoServiceHost,
|
||||
responseBodyResolvers,
|
||||
transformURLSearchParam,
|
||||
} from 'utils/networkService'
|
||||
import { resolveHeaderLink } from './utils'
|
||||
|
||||
const API_ENDPOINT = `${window.location.origin}/api/v4`
|
||||
|
||||
function isAPIRateLimitExceeded(content: JSONValue) {
|
||||
return (
|
||||
is.JSON.object(content) &&
|
||||
content?.['documentation_url'] === 'https://developer.github.com/v3/#rate-limiting'
|
||||
)
|
||||
}
|
||||
|
||||
function isEmptyProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Git Repository is empty.'
|
||||
}
|
||||
|
||||
function isBlockedProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Repository access blocked'
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
url: string,
|
||||
{
|
||||
accessToken,
|
||||
}: {
|
||||
accessToken?: string
|
||||
} = {},
|
||||
bodyResolver: (response: Response) => Async<T> = responseBodyResolvers.json,
|
||||
) {
|
||||
const headers = {} as HeadersInit & {
|
||||
Authorization?: string
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `token ${accessToken}`
|
||||
}
|
||||
|
||||
let res: Response
|
||||
try {
|
||||
res = await fetch(url, { headers })
|
||||
} catch (err) {
|
||||
throw new Error(errors.CONNECTION_BLOCKED)
|
||||
}
|
||||
|
||||
// About res.ok:
|
||||
// True if res.status between 200~299
|
||||
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
|
||||
if (res.ok) return bodyResolver(res)
|
||||
|
||||
if (res.status === 404 || res.status === 401) throw new Error(errors.NOT_FOUND)
|
||||
if (res.status === 403) throw new Error(errors.API_RATE_LIMIT)
|
||||
if (res.status === 500) throw new Error(errors.SERVER_FAULT)
|
||||
|
||||
const content = await responseBodyResolvers.json(res)
|
||||
if (isAPIRateLimitExceeded(content)) throw new Error(errors.API_RATE_LIMIT)
|
||||
if (isEmptyProject(content)) throw new Error(errors.EMPTY_PROJECT)
|
||||
if (isBlockedProject(content)) throw new Error(errors.BLOCKED_PROJECT)
|
||||
|
||||
const message =
|
||||
typeof content === 'object'
|
||||
? (content as Record<string, unknown> | null)?.message
|
||||
: `${content}`
|
||||
throw new Error(`Unknown message content "${message}"`)
|
||||
}
|
||||
|
||||
export async function getRepoMeta(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
accessToken?: string,
|
||||
): Promise<GitLabAPI.Response.MetaData> {
|
||||
const url = `${API_ENDPOINT}/projects/${encodeURIComponent(`${userName}/${repoName}`)}`
|
||||
return await request(url, { accessToken })
|
||||
}
|
||||
|
||||
export async function getTreeData(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
searchParams?: URLSearchParams | NewType,
|
||||
accessToken?: string,
|
||||
): Promise<GitLabAPI.Response.TreeData> {
|
||||
return (await requestTreeData(userName, repoName, searchParams, accessToken)).json()
|
||||
}
|
||||
|
||||
type NewType = {
|
||||
ref?: string
|
||||
path?: string
|
||||
recursive?: boolean
|
||||
id?: string
|
||||
page?: number
|
||||
per_page?: number
|
||||
pagination?: 'legacy'
|
||||
}
|
||||
|
||||
export function requestTreeData(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
searchParams?: URLSearchParams | NewType,
|
||||
accessToken?: string,
|
||||
) {
|
||||
const url = `${API_ENDPOINT}/projects/${encodeURIComponent(
|
||||
`${userName}/${repoName}`,
|
||||
)}/repository/tree/?${
|
||||
searchParams instanceof URLSearchParams
|
||||
? searchParams
|
||||
: searchParams
|
||||
? transformURLSearchParam(searchParams)
|
||||
: ''
|
||||
}`
|
||||
return request(url, { accessToken }, responseBodyResolvers.asIs)
|
||||
}
|
||||
|
||||
export async function getBlobData(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
sha: string,
|
||||
accessToken?: string,
|
||||
): Promise<GitLabAPI.Response.BlobData> {
|
||||
const url = `${API_ENDPOINT}/projects/${encodeURIComponent(
|
||||
`${userName}/${repoName}`,
|
||||
)}/repository/files/?${new URLSearchParams({})}`
|
||||
return await request(url, { accessToken })
|
||||
}
|
||||
|
||||
export async function OAuth(code: string): Promise<string | null> {
|
||||
const endpoint = `https://${gitakoServiceHost}/oauth/gitee?${new URLSearchParams({ code })}`
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'post',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const body = await res.json()
|
||||
const accessToken = body?.accessToken
|
||||
if (typeof accessToken === 'string') return accessToken
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getPaginatedData<T>(
|
||||
initialParams: URLSearchParams,
|
||||
sendRequest: (params: URLSearchParams) => Promise<Response>,
|
||||
) {
|
||||
const responses: Response[] = []
|
||||
|
||||
const run = async (params: URLSearchParams) => {
|
||||
const response = await sendRequest(params)
|
||||
responses.push(response)
|
||||
|
||||
const headerLink = response.headers.get('link')
|
||||
if (headerLink) {
|
||||
const rels = resolveHeaderLink(headerLink)
|
||||
if (rels?.next) await run(new URL(rels.next).searchParams)
|
||||
}
|
||||
}
|
||||
|
||||
await run(initialParams)
|
||||
|
||||
return Promise.all(responses.map(responseBodyResolvers.json) as T[])
|
||||
}
|
||||
37
src/platforms/GitLab/DOMHelper.ts
Normal file
37
src/platforms/GitLab/DOMHelper.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { $ } from 'utils/$'
|
||||
|
||||
export function isInRepoPage() {
|
||||
const repoHeaderSelector = '.project-highlight-puc'
|
||||
return Boolean($(repoHeaderSelector))
|
||||
}
|
||||
|
||||
export function isInCodePage() {
|
||||
const branchListSelector = '.file-holder'
|
||||
return Boolean($(branchListSelector))
|
||||
}
|
||||
|
||||
export function getCurrentBranch() {
|
||||
const selectedBranchButtonSelector = [
|
||||
'.project-refs-form .dropdown-toggle-text',
|
||||
'.tree-ref-holder .gl-dropdown-button-text',
|
||||
].join(',')
|
||||
const element = $(selectedBranchButtonSelector)
|
||||
if (element) {
|
||||
const partialBranchNameFromInnerText = element.textContent
|
||||
if (!partialBranchNameFromInnerText?.includes('…')) return partialBranchNameFromInnerText
|
||||
}
|
||||
|
||||
raiseError(new Error('cannot get current branch'))
|
||||
}
|
||||
|
||||
const REPO_TYPE_PRIVATE = 'private' as const
|
||||
const REPO_TYPE_PUBLIC = 'public' as const
|
||||
export function getRepoPageType() {
|
||||
const headerSelector = `.git-project-title .icon-lock`
|
||||
return $(
|
||||
headerSelector,
|
||||
() => REPO_TYPE_PRIVATE,
|
||||
() => REPO_TYPE_PUBLIC,
|
||||
)
|
||||
}
|
||||
64
src/platforms/GitLab/Request.d.ts
vendored
Normal file
64
src/platforms/GitLab/Request.d.ts
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
declare namespace GitLabAPI {
|
||||
namespace Request {}
|
||||
|
||||
namespace Response {
|
||||
type TreeItem = {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
mode: string
|
||||
type: 'blob' | 'commit' | 'tree'
|
||||
}
|
||||
|
||||
type TreeData = TreeItem[]
|
||||
|
||||
type MetaData = {
|
||||
id: number
|
||||
description: string
|
||||
name: string
|
||||
name_with_namespace: string
|
||||
path: string
|
||||
path_with_namespace: string
|
||||
created_at: string
|
||||
default_branch: string
|
||||
tag_list: string[]
|
||||
topics: string[]
|
||||
ssh_url_to_repo: string
|
||||
http_url_to_repo: string
|
||||
web_url: string
|
||||
readme_url: string
|
||||
avatar_url: string
|
||||
forks_count: number
|
||||
star_count: number
|
||||
last_activity_at: string
|
||||
namespace: {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
kind: string // 'group' or other enum
|
||||
full_path: string
|
||||
parent_id: string | null
|
||||
avatar_url: string
|
||||
web_url: string
|
||||
}
|
||||
}
|
||||
|
||||
type BlobData = {
|
||||
file_name: string
|
||||
file_path: string
|
||||
size: number
|
||||
encoding: string
|
||||
content: string
|
||||
content_sha256: string
|
||||
ref: string
|
||||
blob_id: string
|
||||
commit_id: string
|
||||
last_commit_id: string
|
||||
execute_filemode: boolean
|
||||
}
|
||||
|
||||
type OAuth = {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/platforms/GitLab/URLHelper.ts
Normal file
86
src/platforms/GitLab/URLHelper.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { sanitizedLocation } from 'utils/URLHelper'
|
||||
|
||||
export function getPageUrl(
|
||||
userName: string,
|
||||
repoName?: string,
|
||||
branchName?: string,
|
||||
type?: string,
|
||||
path?: string,
|
||||
) {
|
||||
if (path) return `${origin}/${userName}/${repoName}/-/${type}/${branchName}/${path}`
|
||||
if (type) return `${origin}/${userName}/${repoName}/-/${type}/${branchName}`
|
||||
if (branchName) return `${origin}/${userName}/${repoName}/-/${type}`
|
||||
if (repoName) return `${origin}/${userName}/${repoName}`
|
||||
return `${origin}/${userName}`
|
||||
}
|
||||
|
||||
// gitlab-com/www-gitlab-com/-/blob/mb/k8s-migration-wg-closing-remarks/.tool-versions
|
||||
export function parse(): Partial<MetaData> & { path: string[] } {
|
||||
const [
|
||||
,
|
||||
// ignore content before the first '/'
|
||||
userName,
|
||||
repoName,
|
||||
dash, // `-`
|
||||
type,
|
||||
...path // should be [...branchName.split('/'), ...filePath.split('/')]
|
||||
] = unescape(decodeURIComponent(sanitizedLocation.pathname)).split('/')
|
||||
return {
|
||||
userName,
|
||||
repoName,
|
||||
branchName: undefined,
|
||||
type,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSHA() {
|
||||
const { type, path } = parse()
|
||||
return type === 'blob' || type === 'tree' ? path[0] : undefined
|
||||
}
|
||||
|
||||
function isCommitPath(path: string[]) {
|
||||
return isCompleteCommitSHA(path[0])
|
||||
}
|
||||
|
||||
function isCompleteCommitSHA(sha?: string) {
|
||||
return typeof sha === 'string' && /^[abcdef0-9]{40}$/i.test(sha)
|
||||
}
|
||||
|
||||
export function getCurrentPath(branchName = '') {
|
||||
const { path, type } = parse()
|
||||
if (type === 'blob' || type === 'tree') {
|
||||
if (isCommitPath(path)) {
|
||||
// path = commit-SHA/path/to/item
|
||||
path.shift()
|
||||
} else {
|
||||
// path = branch/name/path/to/item or HEAD/path/to/item
|
||||
// HEAD is not a valid branch name. Getting HEAD means being detached.
|
||||
if (path[0] === 'HEAD') path.shift()
|
||||
else {
|
||||
const splitBranchName = branchName.split('/')
|
||||
while (splitBranchName.length) {
|
||||
if (
|
||||
splitBranchName[0] === path[0] ||
|
||||
// Keep consuming as their heads are same
|
||||
(splitBranchName.length === 1 && splitBranchName[0].startsWith(path[0]))
|
||||
// This happens when visiting URLs like /blob/{commitSHA}/path/to/file
|
||||
// and {commitSHA} is shorter than we got from DOM
|
||||
) {
|
||||
splitBranchName.shift()
|
||||
path.shift()
|
||||
} else {
|
||||
raiseError(new Error(`branch name and path prefix not match`), {
|
||||
branchName,
|
||||
path: parse().path,
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return path.map(decodeURIComponent)
|
||||
}
|
||||
return []
|
||||
}
|
||||
181
src/platforms/GitLab/index.ts
Normal file
181
src/platforms/GitLab/index.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { GITEE_OAUTH } from 'env'
|
||||
import { Base64 } from 'js-base64'
|
||||
import { resolveGitModules } from 'utils/gitSubmodule'
|
||||
import { useProgressBar } from 'utils/hooks/useProgressBar'
|
||||
import { gitakoServiceHost, transformURLSearchParam } from 'utils/networkService'
|
||||
import { sortFoldersToFront } from 'utils/treeParser'
|
||||
import * as API from './API'
|
||||
import * as DOMHelper from './DOMHelper'
|
||||
import * as URLHelper from './URLHelper'
|
||||
|
||||
function processTree(tree: TreeNode[]): TreeNode {
|
||||
// nodes are created from items and put onto tree
|
||||
const pathToItem = new Map<string, TreeNode>()
|
||||
tree.forEach(item => pathToItem.set(item.path, item))
|
||||
|
||||
const pathToCreated = new Map<string, TreeNode>()
|
||||
const root: TreeNode = { name: '', path: '', contents: [], type: 'tree' }
|
||||
pathToCreated.set('', root)
|
||||
tree.forEach(item => {
|
||||
// bottom-up search for the deepest node created
|
||||
let path = item.path
|
||||
const itemsToCreateTreeNode: TreeNode[] = []
|
||||
while (path !== '' && !pathToCreated.has(path)) {
|
||||
const item = pathToItem.get(path)
|
||||
if (item) {
|
||||
itemsToCreateTreeNode.push(item)
|
||||
} else {
|
||||
const $item: TreeNode = {
|
||||
name: path.split('/').pop() || '',
|
||||
path,
|
||||
type: 'tree',
|
||||
contents: [],
|
||||
}
|
||||
pathToItem.set(path, $item)
|
||||
itemsToCreateTreeNode.push($item)
|
||||
}
|
||||
// 'a/b' -> 'a'
|
||||
// 'a' -> ''
|
||||
path = path.substring(0, path.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
// top-down create nodes
|
||||
while (itemsToCreateTreeNode.length) {
|
||||
const item = itemsToCreateTreeNode.pop()
|
||||
if (!item) continue
|
||||
const node: TreeNode = item
|
||||
const parentNode = pathToCreated.get(path)
|
||||
if (parentNode) {
|
||||
if (!parentNode.contents) parentNode.contents = []
|
||||
parentNode.contents.push(node)
|
||||
}
|
||||
pathToCreated.set(node.path, node)
|
||||
path = node.path
|
||||
}
|
||||
})
|
||||
|
||||
sortFoldersToFront(root)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
const origin = window.location.origin
|
||||
|
||||
export const GitLab: Platform = {
|
||||
isEnterprise() {
|
||||
return !origin.endsWith('gitlab.com')
|
||||
},
|
||||
resolvePartialMetaData() {
|
||||
if (!DOMHelper.isInRepoPage()) {
|
||||
return null
|
||||
}
|
||||
|
||||
let branchName
|
||||
if (DOMHelper.isInCodePage()) {
|
||||
// not working well with non-branch blob
|
||||
// cannot handle '/' split branch name, should not use when possibly on branch page
|
||||
branchName = (DOMHelper.getCurrentBranch() || URLHelper.parseSHA())?.trim()
|
||||
}
|
||||
|
||||
const { userName, repoName, type } = URLHelper.parse()
|
||||
if (!userName || !repoName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metaData = {
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
branchName,
|
||||
}
|
||||
return metaData
|
||||
},
|
||||
async getDefaultBranchName({ userName, repoName }, accessToken) {
|
||||
const data = await API.getRepoMeta(userName, repoName, accessToken)
|
||||
return data.default_branch
|
||||
},
|
||||
resolveUrlFromMetaData({ userName, repoName, branchName }) {
|
||||
const repoUrl = `${origin}/${userName}/${repoName}`
|
||||
const userUrl = `${origin}/${userName}`
|
||||
const branchUrl = `${repoUrl}/-/tree/${branchName}`
|
||||
return {
|
||||
repoUrl,
|
||||
userUrl,
|
||||
branchUrl,
|
||||
}
|
||||
},
|
||||
async getTreeData(metaData, path, recursive, accessToken) {
|
||||
const { userName, repoName, branchName } = metaData
|
||||
|
||||
const treeData = (
|
||||
await API.getPaginatedData<GitLabAPI.Response.TreeItem>(
|
||||
transformURLSearchParam({
|
||||
ref: branchName,
|
||||
path,
|
||||
}),
|
||||
params => API.requestTreeData(userName, repoName, params, accessToken),
|
||||
)
|
||||
).flat()
|
||||
|
||||
const root = processTree(
|
||||
treeData.map(item => ({
|
||||
path: item.path,
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
url:
|
||||
item.type && item.path
|
||||
? URLHelper.getPageUrl(
|
||||
metaData.userName,
|
||||
metaData.repoName,
|
||||
metaData.branchName,
|
||||
item.type,
|
||||
item.path,
|
||||
)
|
||||
: undefined,
|
||||
contents: item.type === 'tree' ? [] : undefined,
|
||||
sha: item.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const gitModules = root.contents?.find(item => item.name === '.gitmodules')
|
||||
if (gitModules) {
|
||||
if (metaData.userName && metaData.repoName && gitModules.sha) {
|
||||
const blobData = await API.getBlobData(
|
||||
metaData.repoName,
|
||||
metaData.userName,
|
||||
gitModules.sha,
|
||||
accessToken,
|
||||
)
|
||||
|
||||
if (blobData && blobData.encoding === 'base64' && blobData.content) {
|
||||
await resolveGitModules(root, Base64.decode(blobData.content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { root, defer: true }
|
||||
},
|
||||
shouldExpandSideBar() {
|
||||
return DOMHelper.isInCodePage()
|
||||
},
|
||||
getCurrentPath(branchName) {
|
||||
return URLHelper.getCurrentPath(branchName)
|
||||
},
|
||||
getOAuthLink() {
|
||||
const params = new URLSearchParams({
|
||||
client_id: GITEE_OAUTH.clientId,
|
||||
scope: 'projects',
|
||||
response_type: 'code',
|
||||
redirect_uri: `https://${gitakoServiceHost}/redirect/?${new URLSearchParams({
|
||||
redirect: window.location.href,
|
||||
})}`,
|
||||
})
|
||||
return `${origin}/oauth/authorize?${params}`
|
||||
},
|
||||
setOAuth(code) {
|
||||
return API.OAuth(code)
|
||||
},
|
||||
usePlatformHooks() {
|
||||
useProgressBar()
|
||||
},
|
||||
}
|
||||
78
src/platforms/GitLab/utils.ts
Normal file
78
src/platforms/GitLab/utils.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Resolved from response header `link`
|
||||
*
|
||||
* Example:
|
||||
* <https://gitlab.com/api/v4/projects/gitlab-com%2Fwww-gitlab-com/repository/tree?id=gitlab-com%2Fwww-gitlab-com&page=2&pagination=legacy&path=&per_page=100&recursive=true&ref=master>; rel="next",\
|
||||
* <https://gitlab.com/api/v4/projects/gitlab-com%2Fwww-gitlab-com/repository/tree?id=gitlab-com%2Fwww-gitlab-com&page=1&pagination=legacy&path=&per_page=100&recursive=true&ref=master>; rel="first",\
|
||||
* <https://gitlab.com/api/v4/projects/gitlab-com%2Fwww-gitlab-com/repository/tree?id=gitlab-com%2Fwww-gitlab-com&page=308&pagination=legacy&path=&per_page=100&recursive=true&ref=master>; rel="last"
|
||||
*
|
||||
* `rel` existence
|
||||
*
|
||||
* rel | first page | middle page | last page
|
||||
* first| ✔ | ✔ | ✔
|
||||
* last | ✔ | ✔ | ✔
|
||||
* next | ✔ | ✔ |
|
||||
* prev | | ✔ | ✔
|
||||
*
|
||||
*/
|
||||
type Rels = {
|
||||
next?: string
|
||||
last?: string
|
||||
prev?: string
|
||||
first?: string
|
||||
}
|
||||
|
||||
export function resolveHeaderLink(raw: string) {
|
||||
const rels: Rels = {}
|
||||
raw
|
||||
.split(',')
|
||||
.map(part => part.match(/<(.*?)>; *rel="(.*?)"/))
|
||||
.filter((link: RegExpMatchArray | null): link is RegExpMatchArray => !!link)
|
||||
.forEach(([, url, rel]) => {
|
||||
// It's 2022, is there a smarter way to do this in TS?
|
||||
switch (rel) {
|
||||
case 'next':
|
||||
rels.next = url
|
||||
break
|
||||
case 'last':
|
||||
rels.last = url
|
||||
break
|
||||
case 'prev':
|
||||
rels.prev = url
|
||||
break
|
||||
case 'first':
|
||||
rels.first = url
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
if (rels.next && rels.last && !rels.prev && rels.first) {
|
||||
// first page
|
||||
return {
|
||||
first: rels.first,
|
||||
last: rels.last,
|
||||
next: rels.next,
|
||||
position: 'first' as const,
|
||||
}
|
||||
} else if (rels.next && rels.last && rels.prev && rels.first) {
|
||||
// middle page
|
||||
return {
|
||||
first: rels.first,
|
||||
last: rels.last,
|
||||
next: rels.next,
|
||||
prev: rels.prev,
|
||||
position: 'middle' as const,
|
||||
}
|
||||
} else if (!rels.next && !rels.last && rels.prev && rels.first) {
|
||||
// last page
|
||||
return {
|
||||
first: rels.first,
|
||||
last: rels.last,
|
||||
prev: rels.prev,
|
||||
position: 'last' as const,
|
||||
}
|
||||
} else {
|
||||
// unexpected link header content
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,13 @@ import { dummyPlatformForTypeSafety } from './dummyPlatformForTypeSafety'
|
|||
import { Gitea } from './Gitea'
|
||||
import { Gitee } from './Gitee'
|
||||
import { GitHub } from './GitHub'
|
||||
import { GitLab } from './GitLab'
|
||||
|
||||
const platforms = {
|
||||
GitHub,
|
||||
Gitee,
|
||||
Gitea,
|
||||
GitLab,
|
||||
}
|
||||
|
||||
function resolvePlatform() {
|
||||
|
|
|
|||
|
|
@ -1 +1,32 @@
|
|||
export const gitakoServiceHost = 'gitako.enix.one'
|
||||
|
||||
export const responseBodyResolvers = {
|
||||
asIs: (response: Response) => response,
|
||||
json(response: Response) {
|
||||
const contentType = response.headers.get('Content-Type') || response.headers.get('content-type')
|
||||
if (contentType?.includes('application/json')) return response.json()
|
||||
throw new Error(`Response content type is "${contentType}"`)
|
||||
},
|
||||
}
|
||||
|
||||
export function transformURLSearchParam(source: Record<string, unknown>) {
|
||||
return Object.entries(source).reduce((param, [key, value]) => {
|
||||
if (Array.isArray(value))
|
||||
for (const i of value) param.append(key, stringifyValueForURLSearchParam(i))
|
||||
else param.append(key, stringifyValueForURLSearchParam(value))
|
||||
|
||||
return param
|
||||
}, new URLSearchParams())
|
||||
}
|
||||
|
||||
export function stringifyValueForURLSearchParam(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
if (typeof value === 'boolean') return `${value}`
|
||||
if (typeof value === 'number') {
|
||||
if (Number.isNaN(value)) return ''
|
||||
return `${value}`
|
||||
}
|
||||
if (typeof value === null) return 'null'
|
||||
|
||||
return ''
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue