svelte: Add pagination to commit page (#58847)

This commit adds pagination to the commits page and moves all GraphQL codegen logic into the packages `vite.config.ts` file.
This commit is contained in:
Felix Kling 2023-12-14 10:12:27 +01:00 committed by GitHub
parent afa86eb401
commit 7ae9d65c22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1217 additions and 211 deletions

View File

@ -1,14 +1,13 @@
import { readFileSync } from 'fs'
import path from 'path'
import { CodegenConfig, generate } from '@graphql-codegen/cli'
import { type CodegenConfig, generate } from '@graphql-codegen/cli'
import { glob } from 'glob'
import { GraphQLError } from 'graphql'
const ROOT_FOLDER = path.resolve(__dirname, '../../../')
const WEB_FOLDER = path.resolve(ROOT_FOLDER, './client/web')
const SVELTEKIT_FOLDER = path.resolve(ROOT_FOLDER, './client/web-sveltekit')
const BROWSER_FOLDER = path.resolve(ROOT_FOLDER, './client/browser')
const SHARED_FOLDER = path.resolve(ROOT_FOLDER, './client/shared')
const VSCODE_FOLDER = path.resolve(ROOT_FOLDER, './client/vscode')
@ -23,8 +22,6 @@ const WEB_DOCUMENTS_GLOB = [
`!${WEB_FOLDER}/src/end-to-end/**/*.*`, // TODO(bazel): can remove when non-bazel dropped
]
const SVELTEKIT_DOCUMENTS_GLOB = [`${SVELTEKIT_FOLDER}/src/lib/**/*.ts`]
const BROWSER_DOCUMENTS_GLOB = [
`${BROWSER_FOLDER}/src/**/*.{ts,tsx}`,
`!${BROWSER_FOLDER}/src/end-to-end/**/*.*`, // TODO(bazel): can remove when non-bazel dropped
@ -41,11 +38,11 @@ const GLOBS: Record<string, string[]> = {
SharedGraphQlOperations: SHARED_DOCUMENTS_GLOB,
VSCodeGraphQlOperations: VSCODE_DOCUMENTS_GLOB,
WebGraphQlOperations: WEB_DOCUMENTS_GLOB,
SvelteKitGraphQlOperations: SVELTEKIT_DOCUMENTS_GLOB,
}
const EXTRA_PLUGINS: Record<string, string[]> = {
SharedGraphQlOperations: ['typescript-apollo-client-helpers'],
SvelteKitGraphQlOperations: ['typed-document-node'],
}
const SHARED_PLUGINS = [
@ -67,10 +64,6 @@ export const ALL_INPUTS: Input[] = [
interfaceNameForOperations: 'WebGraphQlOperations',
outputPath: path.join(WEB_FOLDER, './src/graphql-operations.ts'),
},
{
interfaceNameForOperations: 'SvelteKitGraphQlOperations',
outputPath: path.join(SVELTEKIT_FOLDER, './src/lib/graphql-operations.ts'),
},
{
interfaceNameForOperations: 'SharedGraphQlOperations',
outputPath: path.join(SHARED_FOLDER, './src/graphql-operations.ts'),
@ -152,7 +145,7 @@ function createCodegenConfig(operations: Input[]): CodegenConfig {
if (require.main === module) {
// Entry point to generate all GraphQL operations files, or a single one.
async function main(args: string[]) {
async function main(args: string[]): Promise<void> {
if (args.length !== 0 && args.length !== 2) {
throw new Error('Usage: [<schemaName> <outputPath>]')
}

View File

@ -9,10 +9,14 @@ node_modules
vite.config.ts
svelte.config.js
playwright.config.ts
src/lib/graphql-operations.ts
/server.js
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
# Generated files
*.gql.d.ts
src/lib/graphql-operations.ts
src/lib/graphql-types.ts

View File

@ -7,3 +7,8 @@ node_modules
.env.*.local
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Generated TypeScript type definitions and GraphQL documents
*.gql.d.ts
src/lib/graphql-types.ts
src/lib/graphql-operations.ts

View File

@ -6,30 +6,6 @@ load("@npm//:vite/package_json.bzl", vite_bin = "bin")
# gazelle:ignore
js_library(
name = "graphql_operations_files",
# Keep in sync with glob in client/shared/dev/generateGraphQlOperations.js
srcs = glob(
["src/**/*.ts"],
[
# TODO: Ignore legacy build generated file as it conflicts with the Bazel
# build. This can be removed after the migration.
"src/lib/graphql-operations.ts",
],
),
visibility = ["//visibility:private"],
)
generate_graphql_operations(
name = "graphql_operations_ts",
srcs = [
":graphql_operations_files",
],
out = "src/lib/graphql-operations.ts",
interface_name = "SvelteKitGraphQlOperations",
visibility = ["//visibility:private"],
)
SRCS = [
"package.json",
"vite.config.ts",
@ -51,18 +27,26 @@ SRCS = [
"**/*.svelte",
"**/*.html",
"**/*.tsx",
"**/*.gql",
]],
[
"src/lib/graphql-operations.ts",
"src/lib/graphql-types.ts",
"src/**/*.gql.d.ts",
],
)
) + glob(["dev/**/*.cjs"])
BUILD_DEPS = [
":graphql_operations_ts",
":node_modules/@faker-js/faker",
":node_modules/@graphql-codegen/cli",
":node_modules/@graphql-codegen/typescript",
":node_modules/@graphql-codegen/typescript-operations",
":node_modules/@graphql-codegen/near-operation-file-preset",
":node_modules/@graphql-tools/utils",
":node_modules/@melt-ui/svelte",
":node_modules/@popperjs/core",
":node_modules/@remix-run/router",
":node_modules/@rollup/plugin-graphql",
":node_modules/@sourcegraph/branded",
":node_modules/@sourcegraph/common",
":node_modules/@sourcegraph/http-client",
@ -74,6 +58,7 @@ BUILD_DEPS = [
":node_modules/@sveltejs/kit",
":node_modules/@types/prismjs",
":node_modules/lodash-es",
":node_modules/graphql",
":node_modules/prismjs",
":node_modules/react",
":node_modules/svelte",
@ -102,6 +87,7 @@ BUILD_DEPS = [
"//:node_modules/react-router-dom",
"//:node_modules/rxjs",
"//:node_modules/uuid",
"//cmd/frontend/graphqlbackend:graphql_schema",
]
CONFIGS = [

View File

@ -0,0 +1,49 @@
// @ts-check
const { visit } = require('graphql')
/**
* Custom version of the `typed-document-node` plugin that only generates the type definitions
* for the documents without generating a parsed version. This is all we need because the
* `@rollup/plugin-graphql` plugin already parses the documents for us.
*
* @param {import('graphql').GraphQLSchema} _schema
* @param {import('@graphql-codegen/plugin-helpers').Types.DocumentFile[]} documents
* @param {{operationResultSuffix?: string}} config
*/
const plugin = (_schema, documents, config) => {
const { operationResultSuffix = '' } = config
/** @type {{name: string}[]} */
const allOperations = []
for (const item of documents) {
if (item.document) {
visit(item.document, {
OperationDefinition: {
enter: node => {
if (node.name && node.name.value) {
allOperations.push({
name: node.name.value,
})
}
},
},
})
}
}
const documentNodes = allOperations.map(
({ name }) =>
`export declare const ${name}: TypedDocumentNode<${name}${operationResultSuffix}, ${name}Variables>`
)
if (documentNodes.length === 0) {
return ''
}
return {
prepend: ["import type { TypedDocumentNode } from '@graphql-typed-document-node/core'\n"],
content: documentNodes.join('\n'),
}
}
module.exports = { plugin }

View File

@ -18,7 +18,14 @@
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/near-operation-file-preset": "^3.0.0",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-tools/utils": "^10.0.11",
"@graphql-typed-document-node/core": "^3.2.0",
"@playwright/test": "1.25.0",
"@rollup/plugin-graphql": "^2.0.4",
"@storybook/addon-essentials": "^7.2.0",
"@storybook/addon-interactions": "^7.2.0",
"@storybook/addon-links": "^7.2.0",
@ -37,6 +44,7 @@
"@types/prismjs": "^1.26.0",
"eslint-plugin-storybook": "^0.6.12",
"eslint-plugin-svelte3": "^4.0.0",
"graphql": "^15.0.0",
"msw": "^1.2.3",
"msw-storybook-addon": "^1.8.0",
"prettier": "^3.0.0",
@ -50,7 +58,6 @@
"svelte-check": "^3.4.6",
"tslib": "2.1.0",
"vite": "^4.4.7",
"vite-plugin-graphql-codegen": "^3.2.3",
"vite-plugin-inspect": "^0.7.35",
"vitest": "^0.33.0"
},
@ -60,12 +67,12 @@
"@popperjs/core": "^2.11.8",
"@remix-run/router": "~1.3.3",
"@sourcegraph/branded": "workspace:*",
"@sourcegraph/client-api": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/shared": "workspace:*",
"@sourcegraph/web": "workspace:*",
"@sourcegraph/wildcard": "workspace:*",
"@sourcegraph/client-api": "workspace:*",
"highlight.js": "^10.0.0",
"lodash-es": "^4.17.21",
"prismjs": "^1.29.0",

View File

@ -0,0 +1,25 @@
fragment Commit on GitCommit {
id
abbreviatedOID
canonicalURL
subject
body
author {
date
person {
name
displayName
email
avatarURL
}
}
committer {
date
person {
name
displayName
email
avatarURL
}
}
}

View File

@ -1,25 +1,44 @@
<script lang="ts">
import { mdiDotsHorizontal } from '@mdi/js'
import type { GitCommitFields } from '$lib/graphql-operations'
import type { Commit } from './Commit.gql'
import Icon from '$lib/Icon.svelte'
import UserAvatar from '$lib/UserAvatar.svelte'
import Timestamp from './Timestamp.svelte'
import Timestamp from '$lib/Timestamp.svelte'
import Tooltip from '$lib/Tooltip.svelte'
export let commit: GitCommitFields
export let commit: Commit
export let alwaysExpanded: boolean = false
function getCommitter({ committer }: Commit): NonNullable<Commit['committer']>['person'] | null {
if (!committer) {
return null
}
// Do not show if committer is GitHub (e.g. squash merge)
if (committer.person.name === 'GitHub' && committer.person.email === 'noreply@github.com') {
return null
}
return committer.person
}
$: commitDate = new Date(commit.committer ? commit.committer.date : commit.author.date)
$: author = commit.author.person
$: committer = getCommitter(commit)
$: authorAvatarTooltip = author.name + (committer ? ' (author)' : '')
let expanded = alwaysExpanded
</script>
<div class="root">
<div class="avatar">
<UserAvatar user={commit.author.person} />
<Tooltip tooltip={authorAvatarTooltip}>
<UserAvatar user={author} />
</Tooltip>
</div>
{#if commit.committer}
{#if committer && committer.name !== author.name}
<div class="avatar">
<UserAvatar user={commit.committer.person} />
<Tooltip tooltip="{committer.name} (committer)">
<UserAvatar user={committer} />
</Tooltip>
</div>
{/if}
<div class="info">

View File

@ -4,7 +4,7 @@
</script>
<div class:center>
<div class="loading-spinner" class:center class:icon-inline={inline} aria-label="loading" aria-live="polite" />
<div class="loading-spinner" class:icon-inline={inline} aria-label="loading" aria-live="polite" />
</div>
<style lang="scss">
@ -12,6 +12,8 @@
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
justify-content: center;
}
.loading-spinner {

View File

@ -1,28 +1,3 @@
<script context="module" lang="ts">
enum Param {
before = '$before',
after = '$after',
last = '$last',
}
export function getPaginationParams(
searchParams: URLSearchParams,
pageSize: number
):
| { first: number; last: null; before: null; after: string | null }
| { first: null; last: number; before: string | null; after: null } {
if (searchParams.has('$before')) {
return { first: null, last: pageSize, before: searchParams.get(Param.before), after: null }
} else if (searchParams.has('$after')) {
return { first: pageSize, last: null, before: null, after: searchParams.get(Param.after) }
} else if (searchParams.has('$last')) {
return { first: null, last: pageSize, before: null, after: null }
} else {
return { first: pageSize, last: null, before: null, after: null }
}
}
</script>
<script lang="ts">
import { mdiPageFirst, mdiPageLast, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
@ -30,14 +5,23 @@
import Icon from './Icon.svelte'
import { Button } from './wildcard'
import { Param } from './Paginator'
export let pageInfo: {
hasPreviousPage: boolean
hasNextPage: boolean
startCursor: string | null
endCursor: string | null
}
export let disabled: boolean
type PageInfo =
// Bidirection pagination
| { hasPreviousPage: boolean; hasNextPage: boolean; startCursor: string | null; endCursor: string | null }
// Unidirection pagination
| {
hasNextPage: boolean
hasPreviousPage: boolean
endCursor: string | null
startCursor?: undefined
previousEndCursor: string | null
}
export let pageInfo: PageInfo
export let disabled: boolean = false
export let showLastpageButton: boolean = true
function urlWithParameter(name: string, value: string | null): string {
const url = new URL($page.url)
@ -59,14 +43,18 @@
let firstPageURL = urlWithParameter('', null)
let lastPageURL = urlWithParameter(Param.last, '')
$: previousPageURL = urlWithParameter(Param.before, pageInfo.startCursor)
$: previousPageURL =
pageInfo.startCursor !== undefined
? urlWithParameter(Param.before, pageInfo.startCursor)
: urlWithParameter(Param.after, pageInfo.previousEndCursor)
$: nextPageURL = urlWithParameter(Param.after, pageInfo.endCursor)
$: firstAndPreviousDisabled = disabled || !pageInfo.hasPreviousPage
$: nextAndLastDisabled = disabled || !pageInfo.hasNextPage
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- The event handler is used for event delegation -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click={preventClickOnDisabledLink}>
<Button variant="secondary" outline>
<a slot="custom" let:className href={firstPageURL} class={className} aria-disabled={firstAndPreviousDisabled}>
@ -89,11 +77,13 @@
Next <Icon svgPath={mdiChevronRight} inline />
</a>
</Button>
<Button variant="secondary" outline>
<a slot="custom" let:className class={className} href={lastPageURL} aria-disabled={nextAndLastDisabled}>
<Icon svgPath={mdiPageLast} inline />
</a>
</Button>
{#if showLastpageButton}
<Button variant="secondary" outline>
<a slot="custom" let:className class={className} href={lastPageURL} aria-disabled={nextAndLastDisabled}>
<Icon svgPath={mdiPageLast} inline />
</a>
</Button>
{/if}
</div>
<style lang="scss">

View File

@ -0,0 +1,23 @@
export enum Param {
before = '$before',
after = '$after',
last = '$last',
}
export function getPaginationParams(
searchParams: URLSearchParams,
pageSize: number
):
| { first: number; last: null; before: null; after: string | null }
| { first: null; last: number; before: string | null; after: null } {
if (searchParams.has('$before')) {
return { first: null, last: pageSize, before: searchParams.get(Param.before), after: null }
}
if (searchParams.has('$after')) {
return { first: pageSize, last: null, before: null, after: searchParams.get(Param.after) }
}
if (searchParams.has('$last')) {
return { first: null, last: pageSize, before: null, after: null }
}
return { first: pageSize, last: null, before: null, after: null }
}

View File

@ -2,7 +2,7 @@ import { query, gql, type NodeFromResult } from '$lib/graphql'
import type { BlobResult, BlobVariables, HighlightResult, HighlightVariables, Scalars } from '$lib/graphql-operations'
interface FetchBlobOptions {
repoID: Scalars['ID']
repoID: Scalars['ID']['input']
commitID: string
filePath: string
disableTimeout?: boolean

View File

@ -174,7 +174,7 @@ const COMMIT_QUERY = gql`
`
interface FetchRepoCommitsArgs {
repoID: Scalars['ID']
repoID: Scalars['ID']['input']
revision: string
filePath?: string
first?: number
@ -213,7 +213,7 @@ export async function fetchRepoCommit(repoId: string, revision: string): Promise
export type RepositoryComparisonDiff = Extract<RepositoryComparisonDiffResult['node'], { __typename?: 'Repository' }>
export async function queryRepositoryComparisonFileDiffs(args: {
repo: Scalars['ID']
repo: Scalars['ID']['input']
base: string | null
head: string | null
first: number | null
@ -270,7 +270,7 @@ export async function queryRepositoryComparisonFileDiffs(args: {
}
export async function fetchDiff(
repoID: Scalars['ID'],
repoID: Scalars['ID']['input'],
revspec: string,
paths: string[] = []
): Promise<FileDiffFields[]> {

View File

@ -75,7 +75,7 @@ export const REPOSITORY_GIT_REFS = gql`
`
export async function queryGitReferences(args: {
repo: Scalars['ID']
repo: Scalars['ID']['input']
first?: number
query?: string
type: GitRefType
@ -102,7 +102,7 @@ interface Data {
hasMoreActiveBranches: boolean
}
export async function queryGitBranchesOverview(args: { repo: Scalars['ID']; first: number }): Promise<Data> {
export async function queryGitBranchesOverview(args: { repo: Scalars['ID']['input']; first: number }): Promise<Data> {
const data = await query<RepositoryGitBranchesOverviewResult, RepositoryGitBranchesOverviewVariables>(
gql`
query RepositoryGitBranchesOverview($repo: ID!, $first: Int!, $withBehindAhead: Boolean!) {

View File

@ -89,7 +89,7 @@ export async function fetchSidebarFileTree({
commitID,
filePath,
}: {
repoID: Scalars['ID']
repoID: Scalars['ID']['input']
commitID: string
filePath: string
}): Promise<{ root: TreeRoot; values: FileTreeNodeValue[] }> {
@ -111,7 +111,7 @@ export async function fetchSidebarFileTree({
}
export type FileTreeLoader = (args: {
repoID: Scalars['ID']
repoID: Scalars['ID']['input']
commitID: string
filePath: string
parent?: FileTreeProvider
@ -120,7 +120,7 @@ export type FileTreeLoader = (args: {
interface FileTreeProviderArgs {
root: NonNullable<GitCommitFieldsWithTree['tree']>
values: FileTreeNodeValue[]
repoID: Scalars['ID']
repoID: Scalars['ID']['input']
commitID: string
loader: FileTreeLoader
parent?: TreeProvider<FileTreeNodeValue>
@ -133,7 +133,7 @@ export class FileTreeProvider implements TreeProvider<FileTreeNodeValue> {
return this.args.root
}
getRepoID(): Scalars['ID'] {
getRepoID(): Scalars['ID']['input'] {
return this.args.repoID
}

View File

@ -44,7 +44,7 @@
)
let treeProvider: FileTreeProvider | null = null
async function updateFileTreeProvider(repoID: Scalars['ID'], commitID: string, parentPath: string) {
async function updateFileTreeProvider(repoID: Scalars['ID']['input'], commitID: string, parentPath: string) {
const result = await data.deferred.fileTree
if (!result) {
treeProvider = null

View File

@ -3,11 +3,22 @@
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
import type { Commits } from './page.gql'
import Paginator from '$lib/Paginator.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
export let data: PageData
const { pending, value: commits, set } = createPromiseStore<PageData['deferred']['commits']>()
const { pending, latestValue: commits, set } = createPromiseStore<Promise<Commits>>()
$: set(data.deferred.commits)
// This is a hack to make backword pagination work. It looks like the cursor
// for the commits connection is simply a counter. So if it's > 0 we know that
// there are are previous pages. We just need to take the page size into account.
const PAGE_SIZE = 20
$: cursor = $commits?.pageInfo.endCursor ? +$commits.pageInfo.endCursor : null
$: hasPreviousPage = cursor !== null && cursor > PAGE_SIZE
$: previousEndCursor = String(cursor === null ? 0 : cursor - PAGE_SIZE - PAGE_SIZE)
</script>
<svelte:head>
@ -15,39 +26,75 @@
</svelte:head>
<section>
<div>
<h2>View commits from this repsitory</h2>
<h3>Changes</h3>
{#if $pending}
Loading...
{:else if $commits}
{#if $pending && !$commits}
<div class="loader">
<LoadingSpinner />
</div>
{:else if $commits}
<div class="commits">
<ul>
{#each $commits as commit (commit.canonicalURL)}
{#each $commits.nodes as commit (commit.canonicalURL)}
<li><Commit {commit} /></li>
{/each}
</ul>
{/if}
</div>
</div>
<div class="paginator">
<Paginator
disabled={$pending}
pageInfo={{
...$commits.pageInfo,
hasPreviousPage,
previousEndCursor,
}}
showLastpageButton={false}
/>
<div class="loader" class:visible={$pending}>
<LoadingSpinner />
</div>
</div>
{/if}
</section>
<style lang="scss">
section {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
> .loader {
flex: 1;
display: flex;
}
}
.commits {
overflow-y: auto;
flex: 1;
}
ul {
list-style: none;
padding: 0;
margin: 0;
flex: 1;
width: 100%;
padding: 1rem;
max-width: var(--viewport-xl);
margin: 0 auto;
--avatar-size: 2.5rem;
}
section {
overflow: auto;
margin-top: 1rem;
}
.paginator {
flex: 0 0 auto;
margin: 1rem auto;
display: flex;
align-items: center;
div {
max-width: 54rem;
margin-left: auto;
margin-right: auto;
.loader {
margin-left: 1rem;
visibility: hidden;
&.visible {
visibility: visible;
}
}
}
li {

View File

@ -1,15 +1,35 @@
import { fetchRepoCommits } from '$lib/repo/api/commits'
import { getPaginationParams } from '$lib/Paginator'
import type { PageLoad } from './$types'
import { CommitsQuery } from './page.gql'
export const load: PageLoad = async ({ parent }) => {
const { resolvedRevision } = await parent()
const pageSize = 20
export const load: PageLoad = async ({ parent, url }) => {
const { resolvedRevision, graphqlClient } = await parent()
const { first, after } = getPaginationParams(url.searchParams, pageSize)
return {
deferred: {
commits: fetchRepoCommits({ repoID: resolvedRevision.repo.id, revision: resolvedRevision.commitID }).then(
result => result?.nodes ?? []
),
commits: graphqlClient
.query({
query: CommitsQuery,
variables: {
repo: resolvedRevision.repo.id,
revspec: resolvedRevision.commitID,
first,
afterCursor: after,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Unable to find repository')
}
if (!result.data.node.commit) {
throw new Error('Unable to find commit')
}
return result.data.node.commit.ancestors
}),
},
}
}

View File

@ -0,0 +1,26 @@
#import "$lib/Commit.gql"
fragment Commits on GitCommitConnection {
nodes {
...Commit
}
pageInfo {
hasNextPage
endCursor
}
}
query CommitsQuery($repo: ID!, $revspec: String!, $first: Int, $afterCursor: String) {
node(id: $repo) {
__typename
... on Repository {
id
commit(rev: $revspec) {
id
ancestors(first: $first, afterCursor: $afterCursor) {
...Commits
}
}
}
}
}

View File

@ -1,4 +1,4 @@
import { getPaginationParams } from '$lib/Paginator.svelte'
import { getPaginationParams } from '$lib/Paginator'
import { fetchContributors } from '$lib/repo/api/contributors'
import type { PageLoad } from './$types'

View File

@ -1,84 +1,129 @@
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { generate, type CodegenConfig } from '@graphql-codegen/cli'
import graphql from '@rollup/plugin-graphql'
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig, mergeConfig, type Plugin, type UserConfig } from 'vite'
import inspect from 'vite-plugin-inspect'
const __dirname = dirname(fileURLToPath(import.meta.url))
async function generateGraphQLOperations(): Promise<Plugin> {
const outputPath = './src/lib/graphql-operations.ts'
const documents = ['src/lib/**/*.{ts,graphql}', '!src/lib/graphql-operations.ts']
// We have to dynamically import this module to not make it a dependency when using
// Bazel
const codegen = (await import('vite-plugin-graphql-codegen')).default
return codegen({
// Keep in sync with client/shared/dev/generateGraphQlOperations.ts
config: {
generates: {
[outputPath]: {
documents: 'src/lib/**/*.{ts,graphql}',
config: {
onlyOperationTypes: true,
noExport: false,
enumValues: '@sourcegraph/shared/src/graphql-operations',
interfaceNameForOperations: 'SvelteKitGraphQlOperations',
},
plugins: [
'../shared/dev/extractGraphQlOperationCodegenPlugin.js',
'typescript',
'typescript-operations',
],
// Generates typescript types for gql-tags and .graphql files
// We don't use vite-plugin-graphql-codegen because it doesn't support watch mode
// when documents are defined separately for every generated file.
// Defining a single set of documents at the top level doesn't work either because
// it would generated unnecessary files (e.g. .qql.d.ts files for .ts file) and also
// caused duplicate code generation issues.
function generateGraphQLTypes(): Plugin {
const codgegenConfig: CodegenConfig = {
generates: {
'./src/lib/graphql-operations.ts': {
documents: ['src/{lib,routes}/**/*.ts', '!src/lib/graphql-{operations,types}.ts'],
config: {
onlyOperationTypes: true,
enumValues: '$lib/graphql-types.ts',
//interfaceNameForOperations: 'SvelteKitGraphQlOperations',
},
plugins: ['typescript', 'typescript-operations'],
},
schema: '../../cmd/frontend/graphqlbackend/*.graphql',
errorsOnly: true,
silent: true,
config: {
// https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-operations#config-api-reference
arrayInputCoercion: false,
preResolveTypes: true,
operationResultSuffix: 'Result',
omitOperationSuffix: true,
namingConvention: {
typeNames: 'keep',
enumValues: 'keep',
transformUnderscore: true,
},
declarationKind: 'interface',
avoidOptionals: {
field: true,
inputValue: false,
object: true,
},
scalars: {
DateTime: 'string',
JSON: 'object',
JSONValue: 'unknown',
GitObjectID: 'string',
JSONCString: 'string',
PublishedValue: "boolean | 'draft'",
BigInt: 'string',
},
'src/lib/graphql-types.ts': {
plugins: ['typescript'],
},
'src/': {
documents: ['src/**/*.gql', '!src/**/*.gql.d.ts'],
preset: 'near-operation-file',
presetConfig: {
baseTypesPath: 'lib/graphql-types.ts',
extension: '.gql.d.ts',
},
config: {
useTypeImports: true,
},
plugins: ['typescript-operations', `${__dirname}/dev/typed-document-node.cjs`],
},
// Top-level documents needs to be expliclity configured, otherwise vite-plugin-graphql-codgen
// won't regenerate on change.
documents,
},
})
schema: '../../cmd/frontend/graphqlbackend/*.graphql',
errorsOnly: true,
config: {
// https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-operations#config-api-reference
arrayInputCoercion: false,
preResolveTypes: true,
operationResultSuffix: 'Result',
omitOperationSuffix: true,
namingConvention: {
typeNames: 'keep',
enumValues: 'keep',
transformUnderscore: true,
},
declarationKind: 'interface',
avoidOptionals: {
field: true,
inputValue: false,
object: true,
},
scalars: {
DateTime: 'string',
JSON: 'object',
JSONValue: 'unknown',
GitObjectID: 'string',
JSONCString: 'string',
PublishedValue: "boolean | 'draft'",
BigInt: 'string',
},
},
}
// Cheap custom function to check whether we should run codegen for the provided path
function shouldRunCodegen(path: string): boolean {
// Do not run codegen for generated files
if (/(graphql-(operations|types)|\.gql\.d)\.ts$/.test(path)) {
return false
}
if (/\.(ts|gql)$/.test(path)) {
return true
}
return false
}
async function codegen(): Promise<void> {
try {
await generate(codgegenConfig, true)
} catch {
// generate already logs errors to the console
// but we still need to catch it otherwise vite will terminate
}
}
return {
name: 'graphql-codegen',
buildStart() {
return codegen()
},
configureServer(server) {
server.watcher.on('add', path => {
if (shouldRunCodegen(path)) {
codegen()
}
})
server.watcher.on('change', path => {
if (shouldRunCodegen(path)) {
codegen()
}
})
},
}
}
export default defineConfig(({ mode }) => {
let config: UserConfig = {
plugins: [
sveltekit(),
// When using bazel the graphql operations fiel is generated
// by bazel targets
process.env.BAZEL ? null : generateGraphQLOperations(),
// Generates typescript types for gql-tags and .graphql files
generateGraphQLTypes(),
inspect(),
// Parses .graphql files and imports them as AST
graphql(),
],
define:
mode === 'test'

View File

@ -16,6 +16,7 @@ js_library(
visibility = [
"//client/backstage-backend/node_modules/@sourcegraph/shared/dev:__pkg__",
"//client/shared/dev:__pkg__",
"//client/web-sveltekit:__pkg__",
],
)

View File

@ -441,6 +441,13 @@
"dependencies": {
"ws": "*"
}
},
"@graphql-codegen/cli@5": {
"peerDependencies": {
"@graphql-codegen/typescript": "*",
"@graphql-codegen/typescript-operations": "*",
"@graphql-codegen/near-operation-file-preset": "*"
}
}
}
},

File diff suppressed because it is too large Load Diff